"""Parallel processing engine for prefact.

This module provides multiprocessing capabilities to scan multiple files
in parallel, significantly improving performance on large codebases.
"""

import hashlib
import multiprocessing
import os
import pickle
import time
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from prefact.config import Config
from prefact.engine import RefactoringEngine
from prefact.models import ScanResult


class ParallelScanTask:
    """A task for parallel scanning."""
    
    def __init__(
        self,
        file_path: Path,
        config_dict: Dict[str, Any],
        rule_ids: List[str],
        cache_enabled: bool = True
    ):
        self.file_path = file_path
        self.config_dict = config_dict
        self.rule_ids = rule_ids
        self.cache_enabled = cache_enabled
        self.file_hash = self._calculate_file_hash()
    
    def _calculate_file_hash(self) -> str:
        """Calculate hash of file content for cache key."""
        try:
            with open(self.file_path, 'rb') as f:
                return hashlib.md5(f.read()).hexdigest()
        except Exception:
            return ""
    
    def execute(self) -> ScanResult:
        """Execute the scan task."""
        # Check cache first if enabled
        if self.cache_enabled:
            from prefact.performance.cache import get_cache
            cache = get_cache()
            cache_key = f"scan:{self.file_path}:{self.file_hash}:{hash(tuple(self.rule_ids))}"
            
            cached_result = cache.get(cache_key)
            if cached_result:
                return pickle.loads(cached_result)
        
        # Perform actual scan
        config = Config.from_dict(self.config_dict)
        engine = RefactoringEngine(config)
        
        result = engine.scan_file(self.file_path, self.rule_ids)
        
        # Cache result if enabled
        if self.cache_enabled:
            cache.set(cache_key, pickle.dumps(result), expire=3600)  # 1 hour
        
        return result


class ParallelEngine:
    """Parallel processing engine for prefact."""
    
    def __init__(self, config: Config):
        self.config = config
        self.max_workers = config.get_rule_option(
            "_performance", 
            "max_workers", 
            min(int(multiprocessing.cpu_count() * 1.5), 16)  # 1.5x CPU count, max 16
        )
        self.chunk_size = config.get_rule_option(
            "_performance", 
            "chunk_size", 
            10
        )
        self.cache_enabled = config.get_rule_option(
            "_performance", 
            "cache", 
            True
        )
    
    def scan_files(
        self, 
        file_paths: List[Path], 
        rule_ids: Optional[List[str]] = None
    ) -> List[ScanResult]:
        """Scan multiple files in parallel."""
        if not file_paths:
            return []
        
        # Use all enabled rules if none specified
        if rule_ids is None:
            rule_ids = self._get_enabled_rule_ids()
        
        # Create tasks
        tasks = [
            ParallelScanTask(
                path, 
                self.config.to_dict(), 
                rule_ids,
                self.cache_enabled
            )
            for path in file_paths
        ]
        
        # Decide execution method based on number of files
        if len(tasks) == 1:
            # Single file - no need for parallel processing
            return [tasks[0].execute()]
        elif len(tasks) <= self.chunk_size:
            # Small batch - use thread pool for faster startup
            return self._scan_with_thread_pool(tasks)
        else:
            # Large batch - use process pool for true parallelism
            return self._scan_with_process_pool(tasks)
    
    def _scan_with_thread_pool(self, tasks: List[ParallelScanTask]) -> List[ScanResult]:
        """Scan using thread pool (for small batches)."""
        results = []
        
        with ProcessPoolExecutor(max_workers=min(self.max_workers, len(tasks))) as executor:
            future_to_task = {
                executor.submit(self._execute_task_wrapper, task): task
                for task in tasks
            }
            
            for future in as_completed(future_to_task):
                task = future_to_task[future]
                try:
                    result = future.result()
                    results.append(result)
                except Exception as e:
                    # Create error result
                    error_result = ScanResult(
                        file=task.file_path,
                        issues=[],
                        fixes=[],
                        errors=[str(e)]
                    )
                    results.append(error_result)
        
        return results
    
    def _scan_with_process_pool(self, tasks: List[ParallelScanTask]) -> List[ScanResult]:
        """Scan using process pool (for large batches)."""
        results = []
        
        # Process in chunks to avoid memory issues
        for i in range(0, len(tasks), self.chunk_size):
            chunk = tasks[i:i + self.chunk_size]
            
            with ProcessPoolExecutor(max_workers=self.max_workers) as executor:
                future_to_task = {
                    executor.submit(self._execute_task_wrapper, task): task
                    for task in chunk
                }
                
                for future in as_completed(future_to_task):
                    task = future_to_task[future]
                    try:
                        result = future.result()
                        results.append(result)
                    except Exception as e:
                        # Create error result
                        error_result = ScanResult(
                            file=task.file_path,
                            issues=[],
                            fixes=[],
                            errors=[str(e)]
                        )
                        results.append(error_result)
        
        return results
    
    @staticmethod
    def _execute_task_wrapper(task: ParallelScanTask) -> ScanResult:
        """Wrapper for executing tasks in separate process."""
        return task.execute()
    
    def _get_enabled_rule_ids(self) -> List[str]:
        """Get list of enabled rule IDs."""
        from prefact.rules.registry import get_lazy_registry
        registry = get_lazy_registry()
        
        enabled_rules = []
        for rule_id in registry.list_available_rules():
            if self.config.is_rule_enabled(rule_id):
                enabled_rules.append(rule_id)
        
        return enabled_rules
    
    def fix_files(
        self, 
        file_paths: List[Path], 
        rule_ids: Optional[List[str]] = None
    ) -> List[ScanResult]:
        """Fix multiple files in parallel."""
        # For fixing, we need to be more careful about file conflicts
        # So we'll process sequentially but in parallel for scanning
        results = []
        
        for file_path in file_paths:
            try:
                config = Config.from_dict(self.config.to_dict())
                engine = RefactoringEngine(config)
                result = engine.run_file(file_path, rule_ids)
                results.append(result)
            except Exception as e:
                error_result = ScanResult(
                    file=file_path,
                    issues=[],
                    fixes=[],
                    errors=[str(e)]
                )
                results.append(error_result)
        
        return results


class ParallelScanner:
    """High-level interface for parallel scanning."""
    
    def __init__(self, config: Config):
        self.config = config
        self.engine = ParallelEngine(config)
    
    def scan_directory(
        self, 
        directory: Path, 
        pattern: str = "**/*.py",
        rule_ids: Optional[List[str]] = None,
        exclude_patterns: Optional[List[str]] = None
    ) -> List[ScanResult]:
        """Scan all Python files in a directory."""
        if exclude_patterns is None:
            exclude_patterns = self.config.exclude
        
        # Find all Python files
        file_paths = []
        for pattern in self.config.include:
            for file_path in directory.glob(pattern):
                if file_path.is_file():
                    # Check exclude patterns
                    if not any(file_path.match(exclude) for exclude in exclude_patterns):
                        file_paths.append(file_path)
        
        # Scan in parallel
        return self.engine.scan_files(file_paths, rule_ids)
    
    def scan_workspace(
        self, 
        rule_ids: Optional[List[str]] = None
    ) -> List[ScanResult]:
        """Scan the entire workspace."""
        return self.scan_directory(self.config.project_root, rule_ids=rule_ids)
    
    def get_performance_stats(self) -> Dict[str, Any]:
        """Get performance statistics."""
        return {
            "max_workers": self.engine.max_workers,
            "chunk_size": self.engine.chunk_size,
            "cache_enabled": self.engine.cache_enabled,
            "cpu_count": multiprocessing.cpu_count(),
        }


# Utility functions for multiprocessing
def init_worker() -> None:
    """Initialize worker process."""
    # Set up worker-specific configuration
    os.environ['PREFACT_WORKER'] = '1'


def scan_file_worker(args: Tuple[Path, Dict[str, Any], List[str]]) -> ScanResult:
    """Worker function for scanning a single file."""
    file_path, config_dict, rule_ids = args
    
    config = Config.from_dict(config_dict)
    engine = RefactoringEngine(config)
    
    return engine.scan_file(file_path, rule_ids)


# Performance monitoring
class PerformanceMonitor:
    """Monitor performance of parallel operations."""
    
    def __init__(self):
        self.stats = {
            "files_scanned": 0,
            "total_time": 0,
            "parallel_time": 0,
            "cache_hits": 0,
            "cache_misses": 0,
        }
    
    def start_timing(self) -> None:
        """Start timing an operation."""
        self.start_time = time.time()
    
    def end_timing(self, files_scanned: int) -> None:
        """End timing an operation."""
        elapsed = time.time() - self.start_time
        self.stats["files_scanned"] += files_scanned
        self.stats["total_time"] += elapsed
    
    def record_cache_hit(self):
        """Record a cache hit."""
        self.stats["cache_hits"] += 1
    
    def record_cache_miss(self):
        """Record a cache miss."""
        self.stats["cache_misses"] += 1
    
    def get_stats(self) -> Dict[str, Any]:
        """Get performance statistics."""
        stats = self.stats.copy()
        
        if stats["files_scanned"] > 0:
            stats["avg_time_per_file"] = stats["total_time"] / stats["files_scanned"]
            stats["files_per_second"] = stats["files_scanned"] / stats["total_time"] if stats["total_time"] > 0 else 0
        
        total_cache_operations = stats["cache_hits"] + stats["cache_misses"]
        if total_cache_operations > 0:
            stats["cache_hit_rate"] = f"{stats['cache_hits'] / total_cache_operations}"
        
        return stats


# Global performance monitor
_performance_monitor: Optional[PerformanceMonitor] = None


def get_performance_monitor() -> PerformanceMonitor:
    """Get the global performance monitor."""
    global _performance_monitor
    if _performance_monitor is None:
        _performance_monitor = PerformanceMonitor()
    return _performance_monitor
