"""
MemoryInfo class to retrieve comprehensive system memory information.
Supports physical RAM, swap memory, and virtualization detection.
No third-party libraries required, uses /proc, /sys, and system commands.
"""

import os
import re
import glob
from typing import Optional, Dict, List, Any, Tuple


class MemoryInfo:
    """Class to retrieve detailed information about system memory."""
    
    def __init__(self):
        self._info_cache: Dict[str, Any] = {}
    
    def get_memory_info(self, refresh: bool = False) -> Dict[str, Any]:
        """
        Get comprehensive memory information.
        Returns dictionary with all available memory information.
        """
        if not refresh and self._info_cache:
            return self._info_cache
        
        info = {
            'physical_memory': self._get_physical_memory_info(),
            'swap_memory': self._get_swap_info(),
            'virtual_memory': self._get_virtual_memory_stats(),
            'memory_modules': self._get_memory_modules_info(),
            'top_memory_processes': self._get_top_memory_processes(),
            'memory_stats': self._get_memory_statistics(),
            'numa_info': self._get_numa_info(),
            'is_virtual': False,
            'virtual_type': None
        }
        
        # Detect virtualization
        self._detect_virtualization(info)
        
        self._info_cache = info
        return info
    
    def _get_physical_memory_info(self) -> Dict[str, Any]:
        """Get physical memory information from /proc/meminfo and /sys."""
        mem_info = {
            'total_bytes': None,
            'total_human': None,
            'available_bytes': None,
            'available_human': None,
            'free_bytes': None,
            'free_human': None,
            'used_bytes': None,
            'used_human': None,
            'used_percent': None,
            'buffers_bytes': None,
            'cached_bytes': None,
            'shared_bytes': None,
            'slab_bytes': None,
            'hugepages_total': None,
            'hugepages_free': None,
            'hugepages_size_bytes': None,
            'hugepages_size_human': None
        }
        
        # Parse /proc/meminfo
        try:
            with open('/proc/meminfo', 'r') as f:
                mem_data = f.read()
            
            # Extract values
            patterns = {
                'total_bytes': r'MemTotal:\s+(\d+)\s+kB',
                'available_bytes': r'MemAvailable:\s+(\d+)\s+kB',
                'free_bytes': r'MemFree:\s+(\d+)\s+kB',
                'buffers_bytes': r'Buffers:\s+(\d+)\s+kB',
                'cached_bytes': r'Cached:\s+(\d+)\s+kB',
                'shared_bytes': r'Shmem:\s+(\d+)\s+kB',
                'slab_bytes': r'Slab:\s+(\d+)\s+kB',
                'hugepages_total': r'HugePages_Total:\s+(\d+)',
                'hugepages_free': r'HugePages_Free:\s+(\d+)',
                'hugepages_size_bytes': r'Hugepagesize:\s+(\d+)\s+kB'
            }
            
            for key, pattern in patterns.items():
                match = re.search(pattern, mem_data, re.MULTILINE)
                if match:
                    value = int(match.group(1))
                    if key.endswith('_bytes'):
                        # Convert KB to bytes
                        value *= 1024
                    mem_info[key] = value
            
            # Calculate used memory
            if mem_info['total_bytes'] and mem_info['available_bytes']:
                mem_info['used_bytes'] = mem_info['total_bytes'] - mem_info['available_bytes']
                mem_info['used_human'] = self._bytes_to_human(mem_info['used_bytes'])
                mem_info['used_percent'] = (mem_info['used_bytes'] / mem_info['total_bytes']) * 100
            
            # Convert bytes to human readable
            for key in ['total_bytes', 'available_bytes', 'free_bytes', 'buffers_bytes', 
                       'cached_bytes', 'shared_bytes', 'slab_bytes']:
                if mem_info.get(key):
                    human_key = key.replace('_bytes', '_human')
                    mem_info[human_key] = self._bytes_to_human(mem_info[key])
            
            if mem_info.get('hugepages_size_bytes'):
                mem_info['hugepages_size_human'] = self._bytes_to_human(mem_info['hugepages_size_bytes'])
        
        except Exception as e:
            pass
        
        return mem_info
    
    def _get_swap_info(self) -> Dict[str, Any]:
        """Get swap memory information."""
        swap_info = {
            'total_bytes': None,
            'total_human': None,
            'free_bytes': None,
            'free_human': None,
            'used_bytes': None,
            'used_human': None,
            'used_percent': None,
            'swap_devices': []
        }
        
        # Parse /proc/meminfo for swap totals
        try:
            with open('/proc/meminfo', 'r') as f:
                mem_data = f.read()
            
            total_match = re.search(r'SwapTotal:\s+(\d+)\s+kB', mem_data)
            free_match = re.search(r'SwapFree:\s+(\d+)\s+kB', mem_data)
            
            if total_match:
                swap_info['total_bytes'] = int(total_match.group(1)) * 1024
                swap_info['total_human'] = self._bytes_to_human(swap_info['total_bytes'])
            
            if free_match:
                swap_info['free_bytes'] = int(free_match.group(1)) * 1024
                swap_info['free_human'] = self._bytes_to_human(swap_info['free_bytes'])
            
            if swap_info['total_bytes'] and swap_info['free_bytes']:
                swap_info['used_bytes'] = swap_info['total_bytes'] - swap_info['free_bytes']
                swap_info['used_human'] = self._bytes_to_human(swap_info['used_bytes'])
                if swap_info['total_bytes'] > 0:
                    swap_info['used_percent'] = (swap_info['used_bytes'] / swap_info['total_bytes']) * 100
        
        except Exception:
            pass
        
        # Get individual swap devices from /proc/swaps
        try:
            with open('/proc/swaps', 'r') as f:
                lines = f.readlines()[1:]  # Skip header
                for line in lines:
                    parts = line.strip().split()
                    if len(parts) >= 4:
                        swap_dev = {
                            'device': parts[0],
                            'type': parts[1],
                            'size_bytes': int(parts[2]) * 1024,
                            'size_human': self._bytes_to_human(int(parts[2]) * 1024),
                            'used_bytes': int(parts[3]) * 1024,
                            'used_human': self._bytes_to_human(int(parts[3]) * 1024),
                            'priority': int(parts[4]) if len(parts) > 4 else None
                        }
                        swap_info['swap_devices'].append(swap_dev)
        except Exception:
            pass
        
        return swap_info
    
    def _get_virtual_memory_stats(self) -> Dict[str, Any]:
        """Get virtual memory statistics from /proc/vmstat."""
        vm_stats = {
            'pgpgin': None,      # Pages paged in
            'pgpgout': None,     # Pages paged out
            'pswpin': None,      # Pages swapped in
            'pswpout': None,     # Pages swapped out
            'pgfault': None,     # Page faults
            'pgmajfault': None,  # Major page faults
            'pgfree': None,      # Pages freed
            'pgactivate': None,  # Pages activated
            'pgdeactivate': None,# Pages deactivated
            'pglaundry': None,   # Pages laundried
            'pgrefill': None,    # Pages refilled
            'pgsteal': None,     # Pages stolen
            'pgscan_kswapd': None,   # Pages scanned by kswapd
            'pgscan_direct': None,   # Pages scanned directly
            'pgscan_khugepaged': None, # Pages scanned by khugepaged
        }
        
        try:
            with open('/proc/vmstat', 'r') as f:
                for line in f:
                    parts = line.strip().split()
                    if len(parts) == 2:
                        key, value = parts
                        if key in vm_stats:
                            vm_stats[key] = int(value)
        except Exception:
            pass
        
        return vm_stats
    
    def _get_memory_modules_info(self) -> List[Dict[str, Any]]:
        """
        Get physical memory module information from DMIDECODE or /sys.
        Requires root for full details.
        """
        modules = []
        
        # Try to get from /sys/devices/system/edac/mc/
        try:
            mc_paths = glob.glob('/sys/devices/system/edac/mc/mc*')
            for mc_path in mc_paths:
                dimm_paths = glob.glob(f'{mc_path}/dimm/dimm*')
                for dimm_path in dimm_paths:
                    module = self._read_dimm_info(dimm_path)
                    if module:
                        modules.append(module)
        except Exception:
            pass
        
        # Try dmidecode for detailed module info (requires root)
        if not modules:
            modules = self._get_dmidecode_memory_info()
        
        # If no module info found, try to get from /proc/meminfo
        if not modules:
            modules = self._get_basic_memory_info()
        
        return modules
    
    def _read_dimm_info(self, dimm_path: str) -> Optional[Dict[str, Any]]:
        """Read DIMM information from sysfs."""
        module = {}
        
        try:
            # Try to read various attributes
            attrs = {
                'size': 'size',
                'manufacturer': 'manufacturer',
                'part_number': 'part_num',
                'serial_number': 'serial',
                'rank': 'rank'
            }
            
            for attr_file, attr_name in attrs.items():
                file_path = f'{dimm_path}/{attr_file}'
                if os.path.exists(file_path):
                    with open(file_path, 'r') as f:
                        value = f.read().strip()
                        if value and not value.isspace():
                            module[attr_name] = value
            
            # Parse size (usually in MB)
            if 'size' in module:
                try:
                    size_mb = int(module['size'])
                    module['size_bytes'] = size_mb * 1024 * 1024
                    module['size_human'] = self._bytes_to_human(module['size_bytes'])
                except ValueError:
                    pass
            
            if module:
                module['type'] = 'Unknown'
                return module
        except Exception:
            pass
        
        return None
    
    def _get_dmidecode_memory_info(self) -> List[Dict[str, Any]]:
        """Get detailed memory module info using dmidecode command."""
        modules = []
        
        if not self._check_command_exists('dmidecode'):
            return modules
        
        try:
            # Run dmidecode to get memory information
            result = self._run_command(['dmidecode', '-t', 'memory'], timeout=10)
            if not result:
                return modules
            
            # Parse dmidecode output
            current_module = {}
            in_memory_device = False
            
            for line in result.split('\n'):
                line = line.strip()
                
                if 'Memory Device' in line:
                    if current_module and in_memory_device:
                        modules.append(current_module)
                    current_module = {}
                    in_memory_device = True
                
                elif in_memory_device and ':' in line:
                    key, value = line.split(':', 1)
                    key = key.strip().lower().replace(' ', '_')
                    value = value.strip()
                    
                    if key == 'size':
                        if value != 'No Module Installed' and 'MB' in value:
                            size_mb = int(value.split()[0])
                            current_module['size_bytes'] = size_mb * 1024 * 1024
                            current_module['size_human'] = self._bytes_to_human(current_module['size_bytes'])
                            current_module['size_mb'] = size_mb

                    elif key == 'Locator':
                        current_module['locator'] = value

                    elif key == 'type':
                        current_module['type'] = value
                    
                    elif key == 'speed':
                        current_module['speed'] = value
                    
                    elif key == 'manufacturer':
                        current_module['manufacturer'] = value
                    
                    elif key == 'serial_number':
                        current_module['serial'] = value

                    elif key == 'asset_tag':
                        current_module['asset_tag'] = value
                    
                    elif key == 'part_number':
                        current_module['part_number'] = value
                    
                    elif key == 'rank':
                        current_module['rank'] = value
                    
                    elif key == 'configured_clock_speed':
                        current_module['configured_speed'] = value
                    
                    elif key == 'configured_memory_speed':
                        current_module['configured_memory_speed'] = value

                    elif key == 'minimum_voltage':
                        current_module['minimum_voltage'] = value
                    
                    elif key == 'maximum_voltage':
                        current_module['maximum_voltage'] = value

                    elif key == 'configured_voltage':
                        current_module['configured_voltage'] = value

                    elif key == 'memory_technology':
                        current_module['memory_technology'] = value
                    
                    elif key == 'type_detail':
                        current_module['type_detail'] = value
            
            if current_module and in_memory_device:
                modules.append(current_module)
        
        except Exception:
            pass
        
        return modules
    
    def _get_basic_memory_info(self) -> List[Dict[str, Any]]:
        """Get basic memory information when detailed info is not available."""
        modules = []
        
        # Get total memory size
        total_bytes = None
        try:
            with open('/proc/meminfo', 'r') as f:
                for line in f:
                    if 'MemTotal' in line:
                        total_kb = int(re.search(r'(\d+)', line).group(1))
                        total_bytes = total_kb * 1024
                        break
        except Exception:
            pass
        
        if total_bytes:
            module = {
                'size_bytes': total_bytes,
                'size_human': self._bytes_to_human(total_bytes),
                'type': 'Unknown',
                'manufacturer': 'Unknown',
                'serial': 'Unknown',
                'part_number': 'Unknown',
                'note': 'Detailed information not available (requires root or dmidecode)'
            }
            modules.append(module)
        
        return modules
    
    def _get_top_memory_processes(self, count: int = 10) -> List[Dict[str, Any]]:
        """Get top memory-consuming processes."""
        processes = []
        
        try:
            # Read /proc for process information
            for pid in os.listdir('/proc'):
                if not pid.isdigit():
                    continue
                
                try:
                    # Read process status
                    with open(f'/proc/{pid}/status', 'r') as f:
                        status = f.read()
                    
                    # Get process name
                    name_match = re.search(r'Name:\s+(.+?)\n', status)
                    if not name_match:
                        continue
                    name = name_match.group(1)
                    
                    # Get memory usage (VmRSS in kB)
                    vmrss_match = re.search(r'VmRSS:\s+(\d+)\s+kB', status)
                    if not vmrss_match:
                        continue
                    memory_kb = int(vmrss_match.group(1))
                    
                    if memory_kb > 0:
                        processes.append({
                            'pid': int(pid),
                            'name': name,
                            'memory_kb': memory_kb,
                            'memory_bytes': memory_kb * 1024,
                            'memory_human': self._bytes_to_human(memory_kb * 1024)
                        })
                
                except (IOError, OSError, ValueError):
                    continue
        
        except Exception:
            pass
        
        # Sort by memory usage (descending) and return top N
        processes.sort(key=lambda x: x['memory_kb'], reverse=True)
        return processes[:count]
    
    def _get_memory_statistics(self) -> Dict[str, Any]:
        """Get memory statistics and pressure information."""
        stats = {
            'load_average': None,
            'memory_pressure': None,
            'swap_pressure': None,
            'io_pressure': None,
            'oom_killer_enabled': None,
            'memory_fragmentation': None
        }
        
        # Get load average
        try:
            with open('/proc/loadavg', 'r') as f:
                load = f.read().strip().split()
                if len(load) >= 3:
                    stats['load_average'] = {
                        '1min': float(load[0]),
                        '5min': float(load[1]),
                        '15min': float(load[2])
                    }
        except Exception:
            pass
        
        # Check if OOM killer is enabled
        try:
            with open('/proc/sys/vm/oom_kill_allocating_task', 'r') as f:
                stats['oom_killer_enabled'] = int(f.read().strip()) == 1
        except Exception:
            pass
        
        # Memory pressure (if using cgroup v2)
        try:
            if os.path.exists('/proc/pressure/memory'):
                with open('/proc/pressure/memory', 'r') as f:
                    content = f.read().strip()
                    stats['memory_pressure'] = self._parse_pressure_info(content)
            
            if os.path.exists('/proc/pressure/swap'):
                with open('/proc/pressure/swap', 'r') as f:
                    content = f.read().strip()
                    stats['swap_pressure'] = self._parse_pressure_info(content)
            
            if os.path.exists('/proc/pressure/io'):
                with open('/proc/pressure/io', 'r') as f:
                    content = f.read().strip()
                    stats['io_pressure'] = self._parse_pressure_info(content)
        except Exception:
            pass
        
        # Memory fragmentation info
        try:
            with open('/proc/buddyinfo', 'r') as f:
                stats['memory_fragmentation'] = f.read().strip()
        except Exception:
            pass
        
        return stats
    
    def _get_numa_info(self) -> Optional[Dict[str, Any]]:
        """Get NUMA (Non-Uniform Memory Access) information."""
        numa_info = {
            'numa_enabled': False,
            'numa_nodes': [],
            'numa_stats': {}
        }
        
        # Check if NUMA is enabled
        if not os.path.exists('/sys/devices/system/node'):
            return None
        
        try:
            # Get NUMA nodes
            node_paths = glob.glob('/sys/devices/system/node/node*')
            if node_paths:
                numa_info['numa_enabled'] = True
                
                for node_path in node_paths:
                    node_id = os.path.basename(node_path).replace('node', '')
                    
                    node_info = {
                        'node_id': int(node_id),
                        'memory_bytes': None,
                        'memory_human': None,
                        'cpus': []
                    }
                    
                    # Get node memory
                    meminfo_path = f'{node_path}/meminfo'
                    if os.path.exists(meminfo_path):
                        with open(meminfo_path, 'r') as f:
                            for line in f:
                                if 'MemTotal' in line:
                                    mem_kb = int(re.search(r'(\d+)', line).group(1))
                                    node_info['memory_bytes'] = mem_kb * 1024
                                    node_info['memory_human'] = self._bytes_to_human(node_info['memory_bytes'])
                                    break
                    
                    # Get CPUs in this node
                    cpulist_path = f'{node_path}/cpulist'
                    if os.path.exists(cpulist_path):
                        with open(cpulist_path, 'r') as f:
                            cpu_list = f.read().strip()
                            node_info['cpus'] = self._parse_cpu_list(cpu_list)
                    
                    numa_info['numa_nodes'].append(node_info)
                
                # Get NUMA statistics
                if os.path.exists('/proc/zoneinfo'):
                    with open('/proc/zoneinfo', 'r') as f:
                        content = f.read()
                        # Parse node-specific stats
                        for node in numa_info['numa_nodes']:
                            node_id = node['node_id']
                            pattern = rf'Node {node_id}.*?pages free\s+(\d+)'
                            match = re.search(pattern, content, re.DOTALL)
                            if match:
                                numa_info['numa_stats'][f'node_{node_id}_free_pages'] = int(match.group(1))
        
        except Exception:
            pass
        
        return numa_info
    
    def _detect_virtualization(self, info: Dict[str, Any]):
        """Detect if system is virtualized."""
        # Check CPU info
        try:
            with open('/proc/cpuinfo', 'r') as f:
                cpuinfo = f.read().lower()
                if 'hypervisor' in cpuinfo:
                    info['is_virtual'] = True
                    if 'kvm' in cpuinfo:
                        info['virtual_type'] = 'KVM'
                    elif 'vmware' in cpuinfo:
                        info['virtual_type'] = 'VMware'
                    elif 'virtualbox' in cpuinfo:
                        info['virtual_type'] = 'VirtualBox'
                    elif 'xen' in cpuinfo:
                        info['virtual_type'] = 'Xen'
                    else:
                        info['virtual_type'] = 'Unknown'
        except Exception:
            pass
        
        # Check DMI info
        if not info['is_virtual']:
            try:
                if os.path.exists('/sys/class/dmi/id/product_name'):
                    with open('/sys/class/dmi/id/product_name', 'r') as f:
                        product = f.read().strip().lower()
                        if 'vmware' in product:
                            info['is_virtual'] = True
                            info['virtual_type'] = 'VMware'
                        elif 'virtualbox' in product:
                            info['is_virtual'] = True
                            info['virtual_type'] = 'VirtualBox'
                        elif 'kvm' in product or 'qemu' in product:
                            info['is_virtual'] = True
                            info['virtual_type'] = 'KVM/QEMU'
            except Exception:
                pass
    
    @staticmethod
    def _parse_pressure_info(content: str) -> Dict[str, Any]:
        """Parse pressure stall information."""
        pressure = {}
        for line in content.split('\n'):
            if line.startswith('some'):
                parts = line.split()
                pressure['some'] = {
                    'avg10': float(parts[1].split('=')[1]),
                    'avg60': float(parts[2].split('=')[1]),
                    'avg300': float(parts[3].split('=')[1]),
                    'total': int(parts[4].split('=')[1])
                }
            elif line.startswith('full'):
                parts = line.split()
                pressure['full'] = {
                    'avg10': float(parts[1].split('=')[1]),
                    'avg60': float(parts[2].split('=')[1]),
                    'avg300': float(parts[3].split('=')[1]),
                    'total': int(parts[4].split('=')[1])
                }
        return pressure
    
    @staticmethod
    def _parse_cpu_list(cpu_list: str) -> List[int]:
        """Parse CPU list string (e.g., '0-3,5,7-9') into list of CPU numbers."""
        cpus = []
        for part in cpu_list.split(','):
            if '-' in part:
                start, end = map(int, part.split('-'))
                cpus.extend(range(start, end + 1))
            else:
                cpus.append(int(part))
        return cpus
    
    def get_memory_summary(self) -> str:
        """Get a human-readable summary of memory information."""
        info = self.get_memory_info()
        
        lines = []
        lines.append("=" * 80)
        lines.append("MEMORY INFORMATION SUMMARY")
        lines.append("=" * 80)
        
        # Physical Memory
        phys = info['physical_memory']
        lines.append("\nPhysical Memory:")
        lines.append(f"  Total: {phys.get('total_human', 'Unknown')}")
        lines.append(f"  Used: {phys.get('used_human', 'Unknown')} ({phys.get('used_percent', 0):.1f}%)")
        lines.append(f"  Available: {phys.get('available_human', 'Unknown')}")
        lines.append(f"  Free: {phys.get('free_human', 'Unknown')}")
        lines.append(f"  Buffers: {phys.get('buffers_human', 'Unknown')}")
        lines.append(f"  Cached: {phys.get('cached_human', 'Unknown')}")
        
        if phys.get('hugepages_total', 0) > 0:
            lines.append(f"  HugePages: {phys['hugepages_total']} total, {phys['hugepages_free']} free")
            lines.append(f"  HugePage Size: {phys.get('hugepages_size_human', 'Unknown')}")
        
        # Swap Memory
        swap = info['swap_memory']
        if swap.get('total_bytes', 0) > 0:
            lines.append("\nSwap Memory:")
            lines.append(f"  Total: {swap.get('total_human', 'Unknown')}")
            lines.append(f"  Used: {swap.get('used_human', 'Unknown')} ({swap.get('used_percent', 0):.1f}%)")
            lines.append(f"  Free: {swap.get('free_human', 'Unknown')}")
            
            if swap.get('swap_devices'):
                lines.append("  Swap Devices:")
                for dev in swap['swap_devices']:
                    lines.append(f"    - {dev['device']} ({dev['type']}): {dev['size_human']}, "
                               f"used: {dev['used_human']}")
        
        # Memory Modules
        modules = info['memory_modules']
        if modules:
            lines.append(f"\nMemory Modules ({len(modules)}):")
            for i, module in enumerate(modules, 1):
                lines.append(f"  Module {i}:")
                if 'size_human' in module:
                    lines.append(f"    Size: {module['size_human']}")
                if 'type' in module:
                    lines.append(f"    Type: {module['type']}")
                if 'speed' in module:
                    lines.append(f"    Speed: {module['speed']}")
                if 'manufacturer' in module and module['manufacturer'] != 'Unknown':
                    lines.append(f"    Manufacturer: {module['manufacturer']}")
                if 'part_number' in module and module['part_number'] != 'Unknown':
                    lines.append(f"    Part Number: {module['part_number']}")
                if 'serial' in module and module['serial'] != 'Unknown':
                    lines.append(f"    Serial: {module['serial']}")
                if 'note' in module:
                    lines.append(f"    Note: {module['note']}")
                if 'configured_voltage' in module:
                    lines.append(f"    Configured Voltage: {module['configured_voltage']}")
                if 'memory_technology' in module:
                    lines.append(f"    Memory Technology: {module['memory_technology']}")
                if 'type_detail' in module:
                    lines.append(f"    Type Detail: {module['type_detail']}")
                if 'minimum_voltage' in module:
                    lines.append(f"    Minimum Voltage: {module['minimum_voltage']}")
                if 'maximum_voltage' in module:
                    lines.append(f"    Maximum Voltage: {module['maximum_voltage']}")
                if 'location' in module:
                    lines.append(f"    Location: {module['location']}")

        
        # Top Memory Processes
        processes = info['top_memory_processes']
        if processes:
            lines.append(f"\nTop {len(processes)} Memory-Consuming Processes:")
            for proc in processes:
                lines.append(f"  PID {proc['pid']:6d} - {proc['name']:20s}: {proc['memory_human']}")
        
        # Virtualization
        if info['is_virtual']:
            lines.append(f"\nVirtualization: {info['virtual_type']}")
        
        # Load Average
        stats = info['memory_stats']
        if stats.get('load_average'):
            la = stats['load_average']
            lines.append(f"\nLoad Average: {la['1min']:.2f} (1min), {la['5min']:.2f} (5min), {la['15min']:.2f} (15min)")
        
        # Memory Pressure (if available)
        if stats.get('memory_pressure'):
            lines.append("\nMemory Pressure:")
            pressure = stats['memory_pressure']
            if 'some' in pressure:
                lines.append(f"  Some: avg10={pressure['some']['avg10']:.2f}, avg60={pressure['some']['avg60']:.2f}, avg300={pressure['some']['avg300']:.2f}")
        
        lines.append("\n" + "=" * 80)
        
        return '\n'.join(lines)
    
    def get_memory_usage_warning(self, threshold_percent: float = 90.0) -> Optional[str]:
        """
        Check if memory usage exceeds threshold and return warning.
        threshold_percent: Threshold percentage for warning (default 90%)
        """
        info = self.get_memory_info()
        phys = info['physical_memory']
        
        if phys.get('used_percent') and phys['used_percent'] >= threshold_percent:
            return (f"WARNING: Memory usage is {phys['used_percent']:.1f}% "
                   f"({phys['used_human']} used of {phys['total_human']} total)")
        
        swap = info['swap_memory']
        if swap.get('used_percent') and swap.get('total_bytes', 0) > 0:
            if swap['used_percent'] >= threshold_percent:
                return (f"WARNING: Swap usage is {swap['used_percent']:.1f}% "
                       f"({swap['used_human']} used of {swap['total_human']} total)")
        
        return None
    
    def refresh(self) -> 'MemoryInfo':
        """Clear cache and refresh memory information."""
        self._info_cache = {}
        return self
    
    @staticmethod
    def _bytes_to_human(bytes_val: Optional[int]) -> str:
        """Convert bytes to human-readable format."""
        if bytes_val is None:
            return "Unknown"
        
        for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']:
            if bytes_val < 1024.0:
                return f"{bytes_val:.2f} {unit}"
            bytes_val /= 1024.0
        return f"{bytes_val:.2f} EB"
    
    @staticmethod
    def _check_command_exists(command: str) -> bool:
        """Check if a command exists in the system."""
        import subprocess
        try:
            subprocess.run(['which', command], capture_output=True, timeout=5)
            return True
        except Exception:
            return False
    
    @staticmethod
    def _run_command(command: List[str], timeout: int = 10) -> Optional[str]:
        """Run a command and return its output."""
        import subprocess
        try:
            result = subprocess.run(
                command,
                capture_output=True,
                text=True,
                timeout=timeout
            )
            return result.stdout if result.returncode == 0 else None
        except Exception:
            return None
    
    def __repr__(self) -> str:
        info = self.get_memory_info()
        phys = info['physical_memory']
        total = phys.get('total_human', 'Unknown')
        used_percent = phys.get('used_percent', 0)
        return f"MemoryInfo(total={total}, used={used_percent:.1f}%)"


# ------------------------------------------------------------------
# Example usage and test
# ------------------------------------------------------------------
if __name__ == '__main__':
    memory = MemoryInfo()
    
    # Print summary
    print(memory.get_memory_summary())
    
    # Get detailed information as dictionary
    print("\n" + "=" * 80)
    print("DETAILED INFORMATION (JSON-like structure)")
    print("=" * 80)
    
    info = memory.get_memory_info()
    
    print("\nPhysical Memory:")
    phys = info['physical_memory']
    for key, value in phys.items():
        if value is not None:
            print(f"  {key}: {value}")
    
    print("\nSwap Memory:")
    swap = info['swap_memory']
    for key, value in swap.items():
        if value is not None and key != 'swap_devices':
            print(f"  {key}: {value}")
    
    if swap.get('swap_devices'):
        print("\n  Swap Devices:")
        for dev in swap['swap_devices']:
            print(f"    - {dev}")
    
    print("\nVirtual Memory Statistics:")
    vm_stats = info['virtual_memory']
    for key, value in vm_stats.items():
        if value is not None:
            print(f"  {key}: {value:,}")
    
    if info['numa_info']:
        print("\nNUMA Information:")
        numa = info['numa_info']
        print(f"  NUMA Enabled: {numa['numa_enabled']}")
        if numa['numa_nodes']:
            print(f"  NUMA Nodes: {len(numa['numa_nodes'])}")
            for node in numa['numa_nodes']:
                print(f"    Node {node['node_id']}: {node.get('memory_human', 'Unknown')}, "
                     f"CPUs: {node['cpus'][:10]}{'...' if len(node['cpus']) > 10 else ''}")
    
    # Check for warnings
    warning = memory.get_memory_usage_warning(threshold_percent=80)
    if warning:
        print(f"\n{warning}")
    else:
        print("\nMemory usage is within normal limits")
    
    # Monitor changes over time (example)
    print("\n" + "=" * 80)
    print("MONITORING MEMORY CHANGES (5 second interval)")
    print("=" * 80)
    
    import time
    for i in range(3):
        phys = memory.get_memory_info()['physical_memory']
        print(f"\nSample {i+1}: Used: {phys['used_human']} ({phys['used_percent']:.1f}%), "
              f"Available: {phys['available_human']}")
        
        if i < 2:
            time.sleep(5)
            memory.refresh()  # Refresh data for next sample