"""
MicroBatchInjector - Injects samples in small batches (max 50) to avoid timeouts.

This engine provides:
- Batch injection with timeout protection (max 8 seconds)
- Multiple injection strategies (round_robin, section_based, pattern, velocity_layered)
- Error handling with retry logic (2 retries)
- Progress tracking and detailed reporting
"""

import time
import logging
from typing import List, Dict, Any, Optional, Callable
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError

logger = logging.getLogger(__name__)


class MicroBatchInjector:
    """
    Injects audio samples into Ableton Live in micro-batches to prevent timeouts.
    
    Features:
    - Max 50 samples per batch
    - 8 second timeout protection per batch
    - 2 retry attempts on failure
    - Multiple injection strategies
    - Detailed progress tracking and reporting
    """
    
    def __init__(self, live_bridge: Optional[Any] = None):
        """
        Initialize the MicroBatchInjector.
        
        Args:
            live_bridge: Optional live bridge instance for Ableton communication
        """
        self.live_bridge = live_bridge
        self._injection_stats = {
            "total_attempts": 0,
            "successful": 0,
            "failed": 0,
            "retried": 0,
            "timeout_count": 0,
            "total_time": 0.0,
            "batches_processed": 0
        }
        self._injection_history: List[Dict[str, Any]] = []
        self._max_batch_size = 50
        self._timeout_seconds = 8.0
        self._max_retries = 2
        
    def inject_batch_50(
        self,
        track_index: int,
        sample_paths: List[str],
        start_bar: float,
        strategy: str = "round_robin"
    ) -> Dict[str, Any]:
        """
        Injects up to 50 samples with timeout protection.
        
        Args:
            track_index: Target track index in Ableton
            sample_paths: List of sample file paths to inject
            start_bar: Starting bar position
            strategy: Injection strategy ("round_robin", "section_based", "pattern", "velocity_layered")
            
        Returns:
            Dict with injection results, stats, and any errors
        """
        start_time = time.time()
        
        # Limit to max 50 samples
        if len(sample_paths) > self._max_batch_size:
            logger.warning(f"Sample count ({len(sample_paths)}) exceeds max batch size ({self._max_batch_size}). Truncating.")
            sample_paths = sample_paths[:self._max_batch_size]
        
        # Generate positions based on strategy
        positions = self._generate_positions(strategy, len(sample_paths), start_bar)
        
        # Execute injection with timeout protection
        result = self._execute_with_timeout(
            self._inject_samples,
            track_index,
            sample_paths,
            positions,
            strategy
        )
        
        elapsed = time.time() - start_time
        self._injection_stats["total_time"] += elapsed
        self._injection_stats["batches_processed"] += 1
        
        # Update stats based on result
        if result.get("success", False):
            self._injection_stats["successful"] += len(sample_paths)
        else:
            self._injection_stats["failed"] += len(sample_paths)
        
        # Record injection history
        self._injection_history.append({
            "track_index": track_index,
            "sample_count": len(sample_paths),
            "strategy": strategy,
            "start_bar": start_bar,
            "success": result.get("success", False),
            "elapsed_time": elapsed,
            "errors": result.get("errors", [])
        })
        
        return {
            "success": result.get("success", False),
            "track_index": track_index,
            "samples_injected": result.get("injected_count", 0),
            "strategy_used": strategy,
            "elapsed_time": elapsed,
            "errors": result.get("errors", []),
            "stats": self._get_current_stats()
        }
    
    def inject_with_strategy(
        self,
        track_index: int,
        samples: List[str],
        positions: List[float],
        strategy: str
    ) -> Dict[str, Any]:
        """
        Injects samples using a specific strategy at given positions.
        
        Args:
            track_index: Target track index
            samples: List of sample file paths
            positions: List of bar positions for each sample
            strategy: Injection strategy name
            
        Returns:
            Dict with injection results
        """
        if len(samples) != len(positions):
            return {
                "success": False,
                "error": f"Mismatch: {len(samples)} samples vs {len(positions)} positions",
                "injected_count": 0
            }
        
        # Apply strategy-specific processing
        processed_samples = self._apply_strategy(samples, positions, strategy)
        
        # Execute injection with retry logic
        return self._inject_with_retries(track_index, processed_samples, positions)
    
    def get_injection_report(self) -> Dict[str, Any]:
        """
        Returns comprehensive injection statistics and history.
        
        Returns:
            Dict with success/failure stats and detailed history
        """
        total_attempts = self._injection_stats["successful"] + self._injection_stats["failed"]
        success_rate = 0.0
        if total_attempts > 0:
            success_rate = (self._injection_stats["successful"] / total_attempts) * 100
        
        avg_time_per_batch = 0.0
        if self._injection_stats["batches_processed"] > 0:
            avg_time_per_batch = self._injection_stats["total_time"] / self._injection_stats["batches_processed"]
        
        return {
            "summary": {
                "total_samples_attempted": total_attempts,
                "successful_injections": self._injection_stats["successful"],
                "failed_injections": self._injection_stats["failed"],
                "success_rate_percent": round(success_rate, 2),
                "retry_attempts": self._injection_stats["retried"],
                "timeout_count": self._injection_stats["timeout_count"],
                "batches_processed": self._injection_stats["batches_processed"],
                "total_time_seconds": round(self._injection_stats["total_time"], 3),
                "avg_time_per_batch": round(avg_time_per_batch, 3)
            },
            "configuration": {
                "max_batch_size": self._max_batch_size,
                "timeout_seconds": self._timeout_seconds,
                "max_retries": self._max_retries
            },
            "history": self._injection_history[-10:]  # Last 10 injections
        }
    
    def _generate_positions(
        self,
        strategy: str,
        sample_count: int,
        start_bar: float
    ) -> List[float]:
        """Generate position array based on strategy."""
        positions = []
        
        if strategy == "round_robin":
            # Evenly spaced positions
            for i in range(sample_count):
                positions.append(start_bar + (i * 1.0))  # 1 bar spacing
                
        elif strategy == "section_based":
            # Group samples into sections (every 4 bars)
            section_size = 4
            for i in range(sample_count):
                section = i // section_size
                position_in_section = i % section_size
                positions.append(start_bar + (section * section_size) + position_in_section)
                
        elif strategy == "pattern":
            # Pattern-based positioning with variable spacing
            pattern = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5]  # Half-beat pattern
            for i in range(sample_count):
                pattern_idx = i % len(pattern)
                bar_offset = (i // len(pattern)) * 4  # New pattern every 4 bars
                positions.append(start_bar + bar_offset + pattern[pattern_idx])
                
        elif strategy == "velocity_layered":
            # Same position, multiple samples (for layering)
            base_position = start_bar
            for i in range(sample_count):
                positions.append(base_position + (i * 0.01))  # Slight offset for layering
                
        else:
            # Default: simple sequential
            for i in range(sample_count):
                positions.append(start_bar + i)
        
        return positions
    
    def _apply_strategy(
        self,
        samples: List[str],
        positions: List[float],
        strategy: str
    ) -> List[str]:
        """Apply strategy-specific sample ordering/selection."""
        if strategy == "round_robin":
            # Cycle through samples in order
            return samples
            
        elif strategy == "section_based":
            # Organize by sections - group similar samples
            section_size = max(1, len(samples) // 4)
            organized = []
            for i, sample in enumerate(samples):
                section_idx = i // section_size
                organized.append(sample)
            return organized
            
        elif strategy == "pattern":
            # Use pattern [0,1,2,1,0,3] for sample selection
            pattern_indices = [0, 1, 2, 1, 0, 3]
            patterned = []
            for i in range(len(samples)):
                pattern_idx = pattern_indices[i % len(pattern_indices)]
                if pattern_idx < len(samples):
                    patterned.append(samples[pattern_idx])
                else:
                    patterned.append(samples[i % len(samples)])
            return patterned
            
        elif strategy == "velocity_layered":
            # Return samples as-is for velocity layering
            return samples
            
        return samples
    
    def _execute_with_timeout(
        self,
        func: Callable,
        *args,
        **kwargs
    ) -> Dict[str, Any]:
        """Execute function with timeout protection."""
        try:
            with ThreadPoolExecutor(max_workers=1) as executor:
                future = executor.submit(func, *args, **kwargs)
                return future.result(timeout=self._timeout_seconds)
        except FutureTimeoutError:
            self._injection_stats["timeout_count"] += 1
            logger.error(f"Injection timed out after {self._timeout_seconds} seconds")
            return {
                "success": False,
                "error": f"Timeout after {self._timeout_seconds}s",
                "injected_count": 0,
                "errors": ["timeout"]
            }
        except Exception as e:
            logger.error(f"Injection error: {e}")
            return {
                "success": False,
                "error": str(e),
                "injected_count": 0,
                "errors": [str(e)]
            }
    
    def _inject_with_retries(
        self,
        track_index: int,
        samples: List[str],
        positions: List[float]
    ) -> Dict[str, Any]:
        """Inject samples with retry logic."""
        last_error = None
        
        for attempt in range(self._max_retries + 1):
            self._injection_stats["total_attempts"] += 1
            
            try:
                result = self._inject_samples(track_index, samples, positions)
                if result.get("success", False):
                    return result
                    
                # Partial success - check how many were injected
                if result.get("injected_count", 0) > 0:
                    logger.warning(f"Partial injection: {result['injected_count']}/{len(samples)}")
                    
            except Exception as e:
                last_error = e
                logger.warning(f"Injection attempt {attempt + 1} failed: {e}")
                
            if attempt < self._max_retries:
                self._injection_stats["retried"] += 1
                time.sleep(0.5 * (attempt + 1))  # Exponential backoff
                
        # All retries exhausted
        return {
            "success": False,
            "error": str(last_error) if last_error else "All retry attempts failed",
            "injected_count": 0,
            "errors": [str(last_error)] if last_error else ["max_retries_exhausted"]
        }
    
    def _inject_samples(
        self,
        track_index: int,
        samples: List[str],
        positions: List[float]
    ) -> Dict[str, Any]:
        """
        Core injection logic - actually creates clips in Ableton.
        
        Args:
            track_index: Target track
            samples: Sample paths
            positions: Bar positions
            
        Returns:
            Dict with success status and injected count
        """
        injected = 0
        errors = []
        
        if not self.live_bridge:
            # No live bridge - simulate injection for testing
            logger.info(f"[SIMULATION] Would inject {len(samples)} samples to track {track_index}")
            return {
                "success": True,
                "injected_count": len(samples),
                "errors": [],
                "simulated": True
            }
        
        # Use live bridge to create clips
        try:
            for i, (sample_path, position) in enumerate(zip(samples, positions)):
                try:
                    # Call live bridge to create arrangement clip
                    if hasattr(self.live_bridge, 'create_arrangement_clip'):
                        self.live_bridge.create_arrangement_clip(
                            track_index=track_index,
                            file_path=sample_path,
                            position=position,
                            length=1.0  # 1 bar default
                        )
                        injected += 1
                    elif hasattr(self.live_bridge, 'create_audio_clip'):
                        self.live_bridge.create_audio_clip(
                            track_index=track_index,
                            file_path=sample_path,
                            position=position
                        )
                        injected += 1
                    else:
                        errors.append(f"Live bridge missing required methods")
                        break
                        
                except Exception as e:
                    errors.append(f"Sample {i} ({sample_path}): {str(e)}")
                    logger.error(f"Failed to inject sample {i}: {e}")
                    
            success = injected > 0
            return {
                "success": success,
                "injected_count": injected,
                "total_attempted": len(samples),
                "errors": errors
            }
            
        except Exception as e:
            logger.error(f"Batch injection failed: {e}")
            return {
                "success": False,
                "injected_count": injected,
                "error": str(e),
                "errors": errors + [str(e)]
            }
    
    def _get_current_stats(self) -> Dict[str, Any]:
        """Get current injection statistics."""
        total = self._injection_stats["successful"] + self._injection_stats["failed"]
        return {
            "total_processed": total,
            "success_rate": round((self._injection_stats["successful"] / max(1, total)) * 100, 2),
            "batches": self._injection_stats["batches_processed"],
            "timeouts": self._injection_stats["timeout_count"]
        }
    
    def reset_stats(self) -> None:
        """Reset all injection statistics."""
        self._injection_stats = {
            "total_attempts": 0,
            "successful": 0,
            "failed": 0,
            "retried": 0,
            "timeout_count": 0,
            "total_time": 0.0,
            "batches_processed": 0
        }
        self._injection_history.clear()
        logger.info("MicroBatchInjector stats reset")


# Convenience function for direct usage
def inject_samples_batch(
    track_index: int,
    sample_paths: List[str],
    start_bar: float = 0.0,
    strategy: str = "round_robin",
    live_bridge: Optional[Any] = None
) -> Dict[str, Any]:
    """
    Convenience function to inject a batch of samples.
    
    Args:
        track_index: Target track index
        sample_paths: List of sample file paths
        start_bar: Starting bar position
        strategy: Injection strategy
        live_bridge: Optional live bridge instance
        
    Returns:
        Injection results dict
    """
    injector = MicroBatchInjector(live_bridge=live_bridge)
    return injector.inject_batch_50(
        track_index=track_index,
        sample_paths=sample_paths,
        start_bar=start_bar,
        strategy=strategy
    )
