#!/usr/bin/env python3
"""
MIRA Process Management System
=============================

This module provides comprehensive process monitoring and cleanup capabilities
for MIRA. It automatically detects and manages orphaned MIRA processes to
prevent resource leaks and ensure system stability.

Key Features:
- Automatic hung process detection and cleanup
- Smart process identification using command line patterns
- Grace period handling for legitimate long-running processes
- Background cleanup integration with MIRA operations
- Process lifecycle tracking and monitoring
- Cross-platform compatibility (Windows, Linux, macOS)

Cross-Platform Support:
- Windows: Uses process.terminate() and process.kill() via psutil
- Unix/Linux/macOS: Uses SIGTERM and SIGKILL signals with psutil fallback
- Signal handling gracefully handles systems without signal support
- Path patterns adapted for Windows backslash vs Unix forward slash

Author: MIRA Process Management System
Version: 1.1 (Cross-Platform Autonomous Process Control)
"""

import os
import sys
import time
import psutil
import json
import datetime
import threading
import platform
from typing import List, Dict, Optional, Set
from dataclasses import dataclass
from pathlib import Path
import logging

# Cross-platform signal handling
try:
    import signal
    HAS_SIGNALS = True
except ImportError:
    HAS_SIGNALS = False

logger = logging.getLogger(__name__)

@dataclass
class MIRAProcess:
    """Represents a MIRA-related process."""
    pid: int
    command: str
    create_time: float
    memory_mb: float
    cpu_percent: float
    status: str
    is_hung: bool = False
    should_kill: bool = False
    process_type: str = "unknown"
    grace_period_minutes: int = 5

class MIRAProcessManager:
    """
    Comprehensive process management system for MIRA that automatically
    detects and cleans up orphaned or hung processes.
    """
    
    def __init__(self, memory_dir: str):
        self.memory_dir = memory_dir
        self.process_tracking_dir = os.path.join(memory_dir, "process_tracking")
        os.makedirs(self.process_tracking_dir, exist_ok=True)
        
        # Process patterns that identify MIRA processes (cross-platform)
        self.mira_patterns = [
            "mira-memory",
            "python-memory",
            "direct_interface.py",
            "adaptive_pattern_evolution",
            "automatic_retrospection_scheduler",
            "lightning_vidmem",
            "conversation_archive",
            "intelligence.py",
            "memory_engine.py",
            "claude_message_intelligence",
            "relationship_intelligence",
            "neural_consciousness_system"
        ]
        
        # Windows-specific patterns (backslashes instead of forward slashes)
        if platform.system() == "Windows":
            self.mira_patterns.extend([
                "mira-memory\\",
                "python-memory\\",
                "direct_interface.py",
                "\\mira-memory\\",
                "\\python-memory\\"
            ])
        
        # Processes we should never kill (protected)
        self.protected_patterns = [
            "mira test",
            "jest",
            "npm test",
            "vscode-jest"
        ]
        
        # Windows-specific protected patterns
        if platform.system() == "Windows":
            self.protected_patterns.extend([
                "jest.exe",
                "npm.exe",
                "node.exe"
            ])
        
        # Process types and their grace periods
        self.process_types = {
            "retrospection_scheduler": {"grace_minutes": 1440, "max_instances": 1},  # 24 hours
            "conversation_archive": {"grace_minutes": 30, "max_instances": 2},
            "lightning_vidmem": {"grace_minutes": 10, "max_instances": 3},
            "direct_interface": {"grace_minutes": 5, "max_instances": 5},
            "intelligence_analysis": {"grace_minutes": 15, "max_instances": 3},
            "test_runner": {"grace_minutes": 60, "max_instances": 10},  # Protected
            "unknown_mira": {"grace_minutes": 5, "max_instances": 2}
        }
    
    def identify_mira_processes(self) -> List[MIRAProcess]:
        """Identify all MIRA-related processes currently running."""
        mira_processes = []
        
        try:
            for process in psutil.process_iter(['pid', 'name', 'cmdline', 'create_time', 'memory_info', 'cpu_percent', 'status']):
                try:
                    info = process.info
                    if not info['cmdline']:
                        continue
                    
                    command = ' '.join(info['cmdline'])
                    
                    # Check if this is a MIRA process
                    is_mira = any(pattern in command.lower() for pattern in self.mira_patterns)
                    
                    if is_mira:
                        # Check if it's protected
                        is_protected = any(pattern in command.lower() for pattern in self.protected_patterns)
                        
                        # Determine process type
                        process_type = self._classify_process_type(command)
                        
                        memory_mb = info['memory_info'].rss / 1024 / 1024 if info['memory_info'] else 0
                        
                        mira_proc = MIRAProcess(
                            pid=info['pid'],
                            command=command[:200] + "..." if len(command) > 200 else command,
                            create_time=info['create_time'],
                            memory_mb=memory_mb,
                            cpu_percent=info['cpu_percent'] or 0,
                            status=info['status'],
                            process_type=process_type
                        )
                        
                        # Set grace period based on process type
                        if process_type in self.process_types:
                            mira_proc.grace_period_minutes = self.process_types[process_type]["grace_minutes"]
                        
                        # Don't mark protected processes for killing
                        if not is_protected:
                            mira_processes.append(mira_proc)
                        
                except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                    continue
                    
        except Exception as e:
            logger.debug(f"Error identifying MIRA processes: {e}")
        
        return mira_processes
    
    def _classify_process_type(self, command: str) -> str:
        """Classify the type of MIRA process based on command line."""
        command_lower = command.lower()
        
        if "retrospection_scheduler" in command_lower or "automatic_retrospection" in command_lower:
            return "retrospection_scheduler"
        elif "conversation_archive" in command_lower:
            return "conversation_archive"
        elif "lightning_vidmem" in command_lower:
            return "lightning_vidmem"
        elif "direct_interface" in command_lower:
            return "direct_interface"
        elif any(intel in command_lower for intel in ["intelligence", "neural_consciousness", "adaptive_pattern"]):
            return "intelligence_analysis"
        elif any(test in command_lower for test in ["test", "jest", "npm"]):
            return "test_runner"
        else:
            return "unknown_mira"
    
    def detect_hung_processes(self, processes: List[MIRAProcess]) -> List[MIRAProcess]:
        """Detect which processes are hung and should be cleaned up."""
        hung_processes = []
        current_time = time.time()
        
        # Group processes by type to check instance limits
        type_counts = {}
        for proc in processes:
            if proc.process_type not in type_counts:
                type_counts[proc.process_type] = []
            type_counts[proc.process_type].append(proc)
        
        for proc in processes:
            # Check age
            age_minutes = (current_time - proc.create_time) / 60
            
            # Check if process exceeded grace period
            if age_minutes > proc.grace_period_minutes:
                proc.is_hung = True
                proc.should_kill = True
                hung_processes.append(proc)
                continue
            
            # Check for too many instances of the same type
            if proc.process_type in self.process_types:
                max_instances = self.process_types[proc.process_type]["max_instances"]
                type_processes = type_counts[proc.process_type]
                
                if len(type_processes) > max_instances:
                    # Sort by age and mark oldest for cleanup
                    type_processes.sort(key=lambda p: p.create_time)
                    for old_proc in type_processes[max_instances:]:
                        old_proc.is_hung = True
                        old_proc.should_kill = True
                        if old_proc not in hung_processes:
                            hung_processes.append(old_proc)
            
            # Check for zombie or unresponsive processes
            if proc.status in ['zombie', 'stopped']:
                proc.is_hung = True
                proc.should_kill = True
                if proc not in hung_processes:
                    hung_processes.append(proc)
            
            # Check for high memory usage without CPU activity (potential memory leak)
            if proc.memory_mb > 500 and proc.cpu_percent < 0.1 and age_minutes > 10:
                proc.is_hung = True
                proc.should_kill = True
                if proc not in hung_processes:
                    hung_processes.append(proc)
        
        return hung_processes
    
    def _terminate_process_cross_platform(self, pid: int) -> bool:
        """Terminate a process in a cross-platform way."""
        try:
            # Use psutil for cross-platform process termination
            process = psutil.Process(pid)
            
            if platform.system() == "Windows":
                # Windows: Use terminate() then kill()
                process.terminate()
                try:
                    process.wait(timeout=3)  # Wait up to 3 seconds
                except psutil.TimeoutExpired:
                    process.kill()  # Force kill if it doesn't terminate
                    try:
                        process.wait(timeout=2)
                    except psutil.TimeoutExpired:
                        pass  # Process may be zombie
            else:
                # Unix/Linux/macOS: Use signals if available
                if HAS_SIGNALS:
                    try:
                        os.kill(pid, signal.SIGTERM)
                        time.sleep(2)
                        
                        # Check if still running
                        if psutil.pid_exists(pid):
                            os.kill(pid, signal.SIGKILL)
                            time.sleep(1)
                    except OSError:
                        pass  # Process already terminated
                else:
                    # Fallback to psutil if signals not available
                    process.terminate()
                    try:
                        process.wait(timeout=3)
                    except psutil.TimeoutExpired:
                        process.kill()
                        try:
                            process.wait(timeout=2)
                        except psutil.TimeoutExpired:
                            pass
            
            return True
            
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            # Process doesn't exist or already terminated
            return True
        except Exception as e:
            logger.debug(f"Failed to terminate process {pid}: {e}")
            return False

    def cleanup_processes(self, processes: List[MIRAProcess], force: bool = False) -> Dict:
        """Clean up the specified processes gracefully."""
        cleanup_stats = {
            "attempted": 0,
            "successful": 0,
            "failed": 0,
            "skipped": 0,
            "processes_cleaned": []
        }
        
        for proc in processes:
            if not proc.should_kill and not force:
                cleanup_stats["skipped"] += 1
                continue
            
            cleanup_stats["attempted"] += 1
            
            success = self._terminate_process_cross_platform(proc.pid)
            
            if success:
                cleanup_stats["successful"] += 1
                cleanup_stats["processes_cleaned"].append({
                    "pid": proc.pid,
                    "type": proc.process_type,
                    "command_preview": proc.command[:100]
                })
                
                logger.info(f"🧹 Cleaned up hung MIRA process: PID {proc.pid} ({proc.process_type})")
            else:
                cleanup_stats["failed"] += 1
                logger.debug(f"Failed to cleanup process {proc.pid}")
        
        return cleanup_stats
    
    def run_automatic_cleanup(self) -> Dict:
        """Run the complete automatic cleanup process."""
        cleanup_report = {
            "timestamp": datetime.datetime.now().isoformat(),
            "total_mira_processes_found": 0,
            "hung_processes_detected": 0,
            "cleanup_stats": {},
            "process_summary": {},
            "recommendations": []
        }
        
        try:
            # Identify all MIRA processes
            mira_processes = self.identify_mira_processes()
            cleanup_report["total_mira_processes_found"] = len(mira_processes)
            
            # Detect hung processes
            hung_processes = self.detect_hung_processes(mira_processes)
            cleanup_report["hung_processes_detected"] = len(hung_processes)
            
            # Create process type summary
            type_summary = {}
            for proc in mira_processes:
                if proc.process_type not in type_summary:
                    type_summary[proc.process_type] = {"count": 0, "hung": 0}
                type_summary[proc.process_type]["count"] += 1
                if proc.is_hung:
                    type_summary[proc.process_type]["hung"] += 1
            
            cleanup_report["process_summary"] = type_summary
            
            # Clean up hung processes
            if hung_processes:
                cleanup_stats = self.cleanup_processes(hung_processes)
                cleanup_report["cleanup_stats"] = cleanup_stats
                
                if cleanup_stats["successful"] > 0:
                    cleanup_report["recommendations"].append(f"Successfully cleaned {cleanup_stats['successful']} hung processes")
                
                if cleanup_stats["failed"] > 0:
                    cleanup_report["recommendations"].append(f"Failed to clean {cleanup_stats['failed']} processes - may need manual intervention")
            else:
                cleanup_report["recommendations"].append("No hung processes detected - system is healthy")
            
            # Check for concerning patterns
            if len(mira_processes) > 10:
                cleanup_report["recommendations"].append("High number of MIRA processes detected - consider investigating process lifecycle")
            
            total_memory = sum(proc.memory_mb for proc in mira_processes)
            if total_memory > 1000:  # 1GB
                cleanup_report["recommendations"].append(f"High memory usage by MIRA processes: {total_memory:.1f}MB")
            
            # Log the cleanup report
            self._log_cleanup_report(cleanup_report)
            
        except Exception as e:
            cleanup_report["error"] = str(e)
            logger.error(f"Automatic cleanup failed: {e}")
        
        return cleanup_report
    
    def _log_cleanup_report(self, report: Dict):
        """Log the cleanup report to tracking files."""
        try:
            # Save detailed report
            report_file = os.path.join(self.process_tracking_dir, f"cleanup_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
            with open(report_file, 'w') as f:
                json.dump(report, f, indent=2)
            
            # Append summary to log
            summary_log = {
                "timestamp": report["timestamp"],
                "processes_found": report["total_mira_processes_found"],
                "hung_detected": report["hung_processes_detected"],
                "cleaned_successfully": report.get("cleanup_stats", {}).get("successful", 0),
                "recommendations_count": len(report["recommendations"])
            }
            
            summary_file = os.path.join(self.process_tracking_dir, "cleanup_summary.jsonl")
            with open(summary_file, 'a') as f:
                f.write(json.dumps(summary_log) + '\n')
                
        except Exception as e:
            logger.debug(f"Failed to log cleanup report: {e}")
    
    def get_process_status(self) -> Dict:
        """Get current status of MIRA processes."""
        mira_processes = self.identify_mira_processes()
        hung_processes = self.detect_hung_processes(mira_processes)
        
        return {
            "total_processes": len(mira_processes),
            "hung_processes": len(hung_processes),
            "process_types": list(set(proc.process_type for proc in mira_processes)),
            "total_memory_mb": sum(proc.memory_mb for proc in mira_processes),
            "avg_cpu_percent": sum(proc.cpu_percent for proc in mira_processes) / len(mira_processes) if mira_processes else 0,
            "oldest_process_age_minutes": max((time.time() - proc.create_time) / 60 for proc in mira_processes) if mira_processes else 0
        }
    
    def emergency_cleanup(self) -> Dict:
        """Emergency cleanup - force kill all non-protected MIRA processes."""
        logger.warning("🚨 Emergency MIRA process cleanup initiated")
        
        mira_processes = self.identify_mira_processes()
        cleanup_stats = self.cleanup_processes(mira_processes, force=True)
        
        emergency_report = {
            "timestamp": datetime.datetime.now().isoformat(),
            "type": "emergency_cleanup",
            "processes_found": len(mira_processes),
            "cleanup_stats": cleanup_stats
        }
        
        self._log_cleanup_report(emergency_report)
        return emergency_report


def get_process_manager(memory_dir: str) -> MIRAProcessManager:
    """Get the MIRA process manager instance."""
    return MIRAProcessManager(memory_dir)


def run_silent_cleanup(memory_dir: str) -> bool:
    """Run silent cleanup and return True if any processes were cleaned."""
    try:
        manager = get_process_manager(memory_dir)
        report = manager.run_automatic_cleanup()
        return report.get("cleanup_stats", {}).get("successful", 0) > 0
    except Exception:
        return False


# Command line interface for testing
if __name__ == "__main__":
    import tempfile
    
    # Test the process management system
    with tempfile.TemporaryDirectory() as temp_dir:
        manager = MIRAProcessManager(temp_dir)
        
        print("🔍 Identifying MIRA processes...")
        mira_processes = manager.identify_mira_processes()
        print(f"Found {len(mira_processes)} MIRA processes")
        
        for proc in mira_processes:
            print(f"  PID {proc.pid}: {proc.process_type} - {proc.command[:80]}...")
        
        print("\n🧹 Running automatic cleanup...")
        report = manager.run_automatic_cleanup()
        
        print(f"Cleanup summary:")
        print(f"  Processes found: {report['total_mira_processes_found']}")
        print(f"  Hung detected: {report['hung_processes_detected']}")
        if 'cleanup_stats' in report:
            print(f"  Successfully cleaned: {report['cleanup_stats']['successful']}")
        
        if report['recommendations']:
            print("Recommendations:")
            for rec in report['recommendations']:
                print(f"  • {rec}")