"""
MultiSampleInjector - Advanced sample injection engine for Arrangement View.

Provides multiple strategies for injecting samples into Ableton Live's Arrangement View:
- Round-robin: Rotates through samples at specified intervals
- Layered: Stacks multiple samples at the same positions
- Pattern-based: Uses numeric patterns to select samples
- Section-based: Different samples per song section

Author: AbletonMCP_AI Senior Architecture v3.0
"""

import random
from typing import List, Dict, Optional, Any
from dataclasses import dataclass, field
from enum import Enum


class RotationMode(Enum):
    """Rotation interval options for round-robin injection."""
    BEAT = "beat"
    BAR = "bar"
    TWO_BARS = "2bars"
    FOUR_BARS = "4bars"
    EIGHT_BARS = "8bars"
    RANDOM = "random"


@dataclass
class InjectionPlan:
    """Pre-calculated injection plan for a set of samples."""
    strategy: str
    samples: List[str]
    positions: List[float]
    sample_indices: List[int]
    metadata: Dict[str, Any] = field(default_factory=dict)


class MultiSampleInjector:
    """
    Advanced injection engine for multiple samples into Arrangement View.
    
    Supports multiple injection strategies:
    - Round-robin: Rotate through samples at intervals
    - Layered: Stack samples at same positions
    - Pattern-based: Use numeric patterns for sample selection
    - Section-based: Different samples per song section
    """
    
    def __init__(self, live_bridge: Optional[Any] = None):
        """
        Initialize the MultiSampleInjector.
        
        Args:
            live_bridge: Optional LiveBridge instance for direct Ableton API access
        """
        self.live_bridge = live_bridge
        self._injection_history: List[Dict] = []
        self._rotation_counter = 0
        
    def inject_round_robin(
        self,
        track_index: int,
        samples: List[str],
        positions: List[float],
        rotation: str = "4bars"
    ) -> Dict:
        """
        Inject samples in round-robin fashion, rotating at specified intervals.
        
        Args:
            track_index: Target track index in Arrangement View
            samples: List of sample file paths to rotate through
            positions: List of bar positions for injection
            rotation: Rotation interval - "beat", "bar", "2bars", "4bars", "8bars", "random"
            
        Returns:
            Dict with status, clips_created count, and method info
            
        Raises:
            ValueError: If samples list is empty or rotation mode is invalid
        """
        try:
            # Validation
            if not samples:
                raise ValueError("Sample list cannot be empty")
            if not positions:
                raise ValueError("Positions list cannot be empty")
            if rotation not in [r.value for r in RotationMode]:
                raise ValueError(f"Invalid rotation mode: {rotation}. Use: {', '.join(r.value for r in RotationMode)}")
            
            # Calculate rotation interval in beats (assuming 4/4 time)
            rotation_beats = self._rotation_to_beats(rotation)
            
            clips_created = 0
            current_sample_idx = 0
            
            for pos in positions:
                # Determine which sample to use based on position and rotation
                if rotation == "random":
                    sample_idx = random.randint(0, len(samples) - 1)
                else:
                    sample_idx = int(pos / rotation_beats) % len(samples)
                
                sample_path = samples[sample_idx]
                
                # Inject the sample
                result = self._inject_single_sample(track_index, sample_path, pos)
                if result.get("status") == "success":
                    clips_created += 1
            
            # Record injection
            injection_record = {
                "method": "round_robin",
                "track_index": track_index,
                "samples_used": len(set(samples)),
                "clips_created": clips_created,
                "rotation": rotation,
                "positions_count": len(positions)
            }
            self._injection_history.append(injection_record)
            
            return {
                "status": "success",
                "clips_created": clips_created,
                "method": "round_robin",
                "rotation": rotation,
                "samples_rotated": len(samples)
            }
            
        except Exception as e:
            return {
                "status": "error",
                "clips_created": 0,
                "method": "round_robin",
                "error": str(e)
            }
    
    def inject_layered(
        self,
        track_index: int,
        samples: List[str],
        positions: List[float],
        layers: int = 3,
        velocity_split: bool = True
    ) -> Dict:
        """
        Inject multiple samples layered at the same positions.
        
        Creates a rich, textured sound by stacking samples with optional
        velocity-based distribution.
        
        Args:
            track_index: Target track index
            samples: List of sample file paths
            positions: List of bar positions
            layers: Number of layers to stack (default 3)
            velocity_split: Whether to apply velocity-based distribution
            
        Returns:
            Dict with status and clips_created count
        """
        try:
            if not samples:
                raise ValueError("Sample list cannot be empty")
            if layers < 1 or layers > len(samples):
                raise ValueError(f"Layers must be between 1 and {len(samples)}")
            
            clips_created = 0
            total_layers = 0
            
            for pos in positions:
                # Select samples for this position
                if velocity_split:
                    # Velocity-based: softer samples first
                    selected = samples[:layers]
                    # Sort by "velocity" (using index as proxy)
                    selected = selected[::-1]  # Reverse for layering
                else:
                    # Random selection for variety
                    selected = random.sample(samples, min(layers, len(samples)))
                
                # Inject each layer
                for i, sample in enumerate(selected):
                    # Apply slight offset for layering effect
                    offset = i * 0.01  # 1/100th beat offset
                    result = self._inject_single_sample(track_index, sample, pos + offset)
                    if result.get("status") == "success":
                        clips_created += 1
                        total_layers += 1
            
            return {
                "status": "success",
                "clips_created": clips_created,
                "method": "layered",
                "total_layers": total_layers,
                "layers_per_position": layers,
                "positions": len(positions)
            }
            
        except Exception as e:
            return {
                "status": "error",
                "clips_created": 0,
                "method": "layered",
                "error": str(e)
            }
    
    def inject_pattern(
        self,
        track_index: int,
        samples: List[str],
        pattern: List[int],
        positions: List[float]
    ) -> Dict:
        """
        Inject samples following a numeric pattern.
        
        Pattern indices map to sample indices. Example:
        - samples = ["kick1.wav", "kick2.wav", "snare.wav", "clap.wav"]
        - pattern = [0, 0, 2, 1, 0, 3, 2, 1]
        - This creates: kick1, kick1, snare, kick2, kick1, clap, snare, kick2
        
        Args:
            track_index: Target track index
            samples: List of sample file paths
            pattern: List of integers mapping to sample indices
            positions: List of bar positions (must match pattern length or be multiple)
            
        Returns:
            Dict with status and clips_created count
        """
        try:
            if not samples:
                raise ValueError("Sample list cannot be empty")
            if not pattern:
                raise ValueError("Pattern cannot be empty")
            if not positions:
                raise ValueError("Positions cannot be empty")
            
            # Validate pattern indices
            max_idx = len(samples) - 1
            for idx in pattern:
                if idx < 0 or idx > max_idx:
                    raise ValueError(f"Pattern index {idx} out of range (0-{max_idx})")
            
            # Calculate repetitions needed
            if len(positions) % len(pattern) != 0:
                # Extend positions to match pattern cycles
                cycles = (len(positions) // len(pattern)) + 1
                extended_pattern = pattern * cycles
                pattern = extended_pattern[:len(positions)]
            else:
                # Repeat pattern to match positions
                repeats = len(positions) // len(pattern)
                pattern = pattern * repeats
            
            clips_created = 0
            pattern_usage = {i: 0 for i in range(len(samples))}
            
            for pos, sample_idx in zip(positions, pattern):
                sample_path = samples[sample_idx]
                result = self._inject_single_sample(track_index, sample_path, pos)
                if result.get("status") == "success":
                    clips_created += 1
                    pattern_usage[sample_idx] += 1
            
            return {
                "status": "success",
                "clips_created": clips_created,
                "method": "pattern",
                "pattern_length": len(pattern),
                "pattern_usage": pattern_usage,
                "positions": len(positions)
            }
            
        except Exception as e:
            return {
                "status": "error",
                "clips_created": 0,
                "method": "pattern",
                "error": str(e)
            }
    
    def inject_section_based(
        self,
        track_index: int,
        samples_by_section: Dict[str, List[str]],
        sections: List[Dict]
    ) -> Dict:
        """
        Inject different samples per song section.
        
        Args:
            track_index: Target track index
            samples_by_section: Dict mapping section names to sample lists
                Example: {"intro": ["kick_soft.wav"], "chorus": ["kick_hard.wav"]}
            sections: List of section definitions
                Example: [{"name": "intro", "start_bar": 0, "end_bar": 8}, ...]
            
        Returns:
            Dict with status and clips_created count per section
        """
        try:
            if not samples_by_section:
                raise ValueError("samples_by_section cannot be empty")
            if not sections:
                raise ValueError("sections list cannot be empty")
            
            total_clips = 0
            section_results = {}
            
            for section in sections:
                section_name = section.get("name", "unnamed")
                start_bar = section.get("start_bar", 0)
                end_bar = section.get("end_bar", start_bar + 8)
                density = section.get("density", 1)  # Clips per bar
                
                # Get samples for this section
                section_samples = samples_by_section.get(section_name, [])
                if not section_samples:
                    section_results[section_name] = {
                        "clips": 0,
                        "skipped": True,
                        "reason": "No samples defined"
                    }
                    continue
                
                # Calculate positions for this section
                duration = end_bar - start_bar
                positions = []
                for bar in range(int(start_bar), int(end_bar)):
                    for d in range(density):
                        pos = bar + (d / density)
                        if pos < end_bar:
                            positions.append(pos)
                
                # Inject samples for this section using round-robin
                result = self.inject_round_robin(
                    track_index=track_index,
                    samples=section_samples,
                    positions=positions,
                    rotation="bar"
                )
                
                section_results[section_name] = {
                    "clips": result.get("clips_created", 0),
                    "samples_used": len(section_samples),
                    "positions": len(positions)
                }
                total_clips += result.get("clips_created", 0)
            
            return {
                "status": "success",
                "clips_created": total_clips,
                "method": "section_based",
                "section_results": section_results,
                "sections_processed": len(sections)
            }
            
        except Exception as e:
            return {
                "status": "error",
                "clips_created": 0,
                "method": "section_based",
                "error": str(e)
            }
    
    def calculate_injection_plan(
        self,
        strategy: str,
        samples: List[str],
        song_length: int
    ) -> Dict:
        """
        Pre-calculate an injection plan without executing it.
        
        Useful for previewing what samples will be used where before
        actual injection.
        
        Args:
            strategy: Injection strategy - "round_robin", "layered", "pattern", "section_based"
            samples: List of sample file paths
            song_length: Song length in bars
            
        Returns:
            Dict with InjectionPlan details and preview
        """
        try:
            if not samples:
                raise ValueError("Sample list cannot be empty")
            if song_length <= 0:
                raise ValueError("Song length must be positive")
            
            valid_strategies = ["round_robin", "layered", "pattern", "section_based"]
            if strategy not in valid_strategies:
                raise ValueError(f"Invalid strategy: {strategy}. Use: {', '.join(valid_strategies)}")
            
            # Generate positions (one per bar for planning)
            positions = list(range(song_length))
            
            # Calculate which samples go where
            sample_indices = []
            if strategy == "round_robin":
                for pos in positions:
                    sample_indices.append(pos % len(samples))
            elif strategy == "layered":
                # All samples at each position
                for pos in positions:
                    sample_indices.append(list(range(len(samples))))
            elif strategy == "pattern":
                # Simple alternating pattern
                pattern = [i % len(samples) for i in range(8)]  # 8-step pattern
                for pos in positions:
                    sample_indices.append(pattern[pos % len(pattern)])
            elif strategy == "section_based":
                # Assume 4-bar sections
                section_size = 4
                for pos in positions:
                    section_idx = (pos // section_size) % len(samples)
                    sample_indices.append(section_idx)
            
            plan = InjectionPlan(
                strategy=strategy,
                samples=samples,
                positions=[float(p) for p in positions],
                sample_indices=sample_indices,
                metadata={
                    "estimated_clips": len(positions),
                    "samples_available": len(samples),
                    "song_length_bars": song_length
                }
            )
            
            # Create preview
            preview = []
            for i, (pos, idx) in enumerate(zip(positions[:16], sample_indices[:16])):  # First 16 bars
                if isinstance(idx, list):
                    sample_names = [samples[j].split("/")[-1] for j in idx]
                    preview.append(f"Bar {pos}: {', '.join(sample_names)}")
                else:
                    sample_name = samples[idx].split("/")[-1]
                    preview.append(f"Bar {pos}: {sample_name}")
            
            return {
                "status": "success",
                "plan": {
                    "strategy": plan.strategy,
                    "samples": [s.split("/")[-1] for s in plan.samples],
                    "positions": plan.positions,
                    "sample_indices": plan.sample_indices,
                    "metadata": plan.metadata
                },
                "preview": preview,
                "total_clips_estimate": len(positions) * (len(samples) if strategy == "layered" else 1)
            }
            
        except Exception as e:
            return {
                "status": "error",
                "plan": None,
                "error": str(e)
            }
    
    def _rotation_to_beats(self, rotation: str) -> float:
        """Convert rotation mode to beat count."""
        rotation_map = {
            "beat": 1.0,
            "bar": 4.0,
            "2bars": 8.0,
            "4bars": 16.0,
            "8bars": 32.0,
            "random": 4.0  # Default for random
        }
        return rotation_map.get(rotation, 16.0)  # Default to 4bars
    
    def _inject_single_sample(
        self,
        track_index: int,
        sample_path: str,
        position: float
    ) -> Dict:
        """
        Inject a single sample at a specific position.
        
        Uses LiveBridge if available, otherwise returns simulation.
        """
        if self.live_bridge:
            try:
                # Use LiveBridge for actual injection
                result = self.live_bridge.create_arrangement_audio_clip(
                    track_index=track_index,
                    file_path=sample_path,
                    start_time=position,
                    length=4.0  # Default 4 beats
                )
                return result
            except Exception as e:
                return {
                    "status": "error",
                    "error": f"LiveBridge injection failed: {str(e)}"
                }
        else:
            # Simulation mode for planning/testing
            return {
                "status": "success",
                "simulated": True,
                "track_index": track_index,
                "sample": sample_path.split("/")[-1],
                "position": position
            }
    
    def get_injection_history(self) -> List[Dict]:
        """Get history of all injections performed."""
        return self._injection_history.copy()
    
    def clear_history(self) -> None:
        """Clear injection history."""
        self._injection_history.clear()
        self._rotation_counter = 0
    
    def validate_samples(self, samples: List[str]) -> Dict:
        """
        Validate that samples are available and accessible.
        
        Returns validation results with any errors found.
        """
        import os
        
        results = {
            "valid": [],
            "invalid": [],
            "total": len(samples)
        }
        
        for sample in samples:
            if os.path.exists(sample):
                results["valid"].append(sample)
            else:
                results["invalid"].append({
                    "path": sample,
                    "error": "File not found"
                })
        
        results["all_valid"] = len(results["invalid"]) == 0
        return results


# Convenience functions for direct usage
def create_injector(live_bridge=None) -> MultiSampleInjector:
    """Factory function to create a MultiSampleInjector instance."""
    return MultiSampleInjector(live_bridge=live_bridge)


def quick_round_robin(
    track_index: int,
    samples: List[str],
    positions: List[float],
    rotation: str = "4bars",
    live_bridge=None
) -> Dict:
    """Quick round-robin injection without creating instance."""
    injector = MultiSampleInjector(live_bridge=live_bridge)
    return injector.inject_round_robin(track_index, samples, positions, rotation)


def quick_layered(
    track_index: int,
    samples: List[str],
    positions: List[float],
    layers: int = 3,
    live_bridge=None
) -> Dict:
    """Quick layered injection without creating instance."""
    injector = MultiSampleInjector(live_bridge=live_bridge)
    return injector.inject_layered(track_index, samples, positions, layers)


def quick_pattern(
    track_index: int,
    samples: List[str],
    pattern: List[int],
    positions: List[float],
    live_bridge=None
) -> Dict:
    """Quick pattern injection without creating instance."""
    injector = MultiSampleInjector(live_bridge=live_bridge)
    return injector.inject_pattern(track_index, samples, pattern, positions)
