"""
AmbienceGenerator - Atmospheric and Ambience Layer System for AbletonMCP_AI

This engine creates atmospheric elements, textures, and ambience layers for
intros, outros, and transitional sections in reggaeton productions.

Key features:
- Layered intro ambience with rain, wash effects, and drones
- Outro fade effects with gradual ambience reduction
- Texture layers for specific song sections
- Drone placement at build positions

Author: AbletonMCP_AI
"""
import os
import logging
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, field

logger = logging.getLogger("AmbienceGenerator")


@dataclass
class AmbienceLayer:
    """Represents a single ambience layer configuration.
    
    Attributes:
        name: Name of the layer
        sample_path: Path to the audio sample
        track_index: Target track index
        start_bar: Start position in bars
        duration_bars: Duration in bars
        volume: Volume level (0.0-1.0)
        pan: Pan position (-1.0 to 1.0)
        fade_in: Fade in duration in bars
        fade_out: Fade out duration in bars
    """
    name: str
    sample_path: str
    track_index: int = 0
    start_bar: float = 0.0
    duration_bars: float = 4.0
    volume: float = 0.7
    pan: float = 0.0
    fade_in: float = 0.0
    fade_out: float = 0.0
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "sample_path": self.sample_path,
            "track_index": self.track_index,
            "start_bar": self.start_bar,
            "duration_bars": self.duration_bars,
            "volume": self.volume,
            "pan": self.pan,
            "fade_in": self.fade_in,
            "fade_out": self.fade_out,
        }


@dataclass
class AmbienceConfiguration:
    """Complete ambience configuration for a section.
    
    Attributes:
        layers: List of ambience layers
        total_duration: Total duration in bars
        section_type: Type of section (intro, outro, bridge, etc.)
        crossfade: Whether to apply crossfading between layers
    """
    layers: List[AmbienceLayer] = field(default_factory=list)
    total_duration: float = 16.0
    section_type: str = "intro"
    crossfade: bool = True
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "layers": [layer.to_dict() for layer in self.layers],
            "total_duration": self.total_duration,
            "section_type": self.section_type,
            "crossfade": self.crossfade,
            "layer_count": len(self.layers),
        }


class AmbienceGenerator:
    """
    Generator for atmospheric ambience and texture layers.
    
    This class creates layered ambience effects for song intros, outros,
    and transitional sections using rain sounds, wash effects, and
    synthesizer drones from the sample library.
    
    Key samples used:
    - lluvia.wav: Rain ambience for atmospheric texture
    - wash.wav: Wash effect for transitions
    - SL2025 drones: Synth pads from SentimientoLatino2025
    - Sub-bass drones: Low-frequency atmospheric elements
    """
    
    def __init__(self, live_bridge):
        """
        Initialize the AmbienceGenerator with a LiveBridge instance.
        
        Args:
            live_bridge: AbletonLiveBridge instance for interacting with Live
        """
        self.live_bridge = live_bridge
        self.logger = logging.getLogger("AmbienceGenerator")
        
        # Sample paths relative to libreria root
        self.libreria_root = "C:\\ProgramData\\Ableton\\Live 12 Suite\\Resources\\MIDI Remote Scripts\\libreria\\reggaeton"
        
        # Standard ambience samples
        self.ambience_samples = {
            "rain": os.path.join(self.libreria_root, "fx", "lluvia.wav"),
            "wash": os.path.join(self.libreria_root, "fx", "wash.wav"),
            "impact": os.path.join(self.libreria_root, "fx", "impact.wav"),
            "fx1": os.path.join(self.libreria_root, "fx", "fx 1 (nes siempre lo usa).flac"),
        }
        
        # SL2025 drone/pad samples
        self.sl2025_drones = [
            os.path.join(self.libreria_root, "SentimientoLatino2025", "01\\LATINOS  -  ONE SHOTS", "Midilatino_PAD_Elevado_C.wav"),
            os.path.join(self.libreria_root, "SentimientoLatino2025", "01\\LATINOS  -  ONE SHOTS", "Midilatino_SYNTH_Mystery_C.wav"),
            os.path.join(self.libreria_root, "SentimientoLatino2025", "01\\LATINOS  -  ONE SHOTS", "Midilatino_SYNTH_Sucio_C.wav"),
            os.path.join(self.libreria_root, "SentimientoLatino2025", "01\\LATINOS  -  ONE SHOTS", "Midilatino_SYNTH_Found_C.wav"),
            os.path.join(self.libreria_root, "SentimientoLatino2025", "01\\LATINOS  -  ONE SHOTS", "Midilatino_STRING_Tension_C.wav"),
            os.path.join(self.libreria_root, "SentimientoLatino2025", "01\\LATINOS  -  ONE SHOTS", "Midilatino_KEY_Largo_C.wav"),
            os.path.join(self.libreria_root, "SentimientoLatino2025", "01\\LATINOS  -  ONE SHOTS", "Midilatino_KEY_Profundo_C.wav"),
        ]
        
        self.logger.info("AmbienceGenerator initialized")
    
    def create_intro_ambience(self, track_index: int, duration_bars: int = 24) -> Dict[str, Any]:
        """
        Create layered intro ambience with multiple atmospheric layers.
        
        Creates a rich atmospheric intro with:
        - Layer 1: lluvia.wav (rain) - continuous atmospheric texture
        - Layer 2: wash.wav (wash effect) - spectral wash
        - Layer 3: Subtle drones from SL2025 - harmonic content
        
        Args:
            track_index: Index of the target track for ambience
            duration_bars: Duration of the intro in bars (default 24)
            
        Returns:
            Dictionary with creation status and layer details
        """
        self.logger.info(f"Creating intro ambience: track={track_index}, duration={duration_bars} bars")
        
        layers_created = []
        configuration = AmbienceConfiguration(
            total_duration=duration_bars,
            section_type="intro",
            crossfade=True
        )
        
        try:
            # Layer 1: Rain (lluvia.wav) - full duration, panned slightly left
            if os.path.exists(self.ambience_samples["rain"]):
                rain_layer = AmbienceLayer(
                    name="Rain_Ambience",
                    sample_path=self.ambience_samples["rain"],
                    track_index=track_index,
                    start_bar=0.0,
                    duration_bars=duration_bars,
                    volume=0.5,
                    pan=-0.3,
                    fade_in=4.0,
                    fade_out=4.0
                )
                configuration.layers.append(rain_layer)
                
                # Inject rain sample
                result = self._inject_sample_at_position(
                    track_index, 
                    self.ambience_samples["rain"],
                    0.0,
                    duration_bars
                )
                if result.get("success"):
                    layers_created.append("rain")
                    self.logger.info("Layer 1: Rain ambience injected")
            
            # Layer 2: Wash effect - offset by 4 bars, panned center
            if os.path.exists(self.ambience_samples["wash"]):
                wash_layer = AmbienceLayer(
                    name="Wash_Effect",
                    sample_path=self.ambience_samples["wash"],
                    track_index=track_index,
                    start_bar=4.0,
                    duration_bars=min(8.0, duration_bars - 4),
                    volume=0.6,
                    pan=0.0,
                    fade_in=2.0,
                    fade_out=2.0
                )
                configuration.layers.append(wash_layer)
                
                result = self._inject_sample_at_position(
                    track_index,
                    self.ambience_samples["wash"],
                    4.0,
                    min(8.0, duration_bars - 4)
                )
                if result.get("success"):
                    layers_created.append("wash")
                    self.logger.info("Layer 2: Wash effect injected")
            
            # Layer 3: SL2025 subtle drone - staggered entry at bar 8
            drone_sample = self._select_random_drone()
            if drone_sample and os.path.exists(drone_sample):
                drone_layer = AmbienceLayer(
                    name="SL2025_Drone",
                    sample_path=drone_sample,
                    track_index=track_index,
                    start_bar=8.0,
                    duration_bars=duration_bars - 8,
                    volume=0.4,
                    pan=0.3,
                    fade_in=4.0,
                    fade_out=8.0
                )
                configuration.layers.append(drone_layer)
                
                result = self._inject_sample_at_position(
                    track_index,
                    drone_sample,
                    8.0,
                    duration_bars - 8
                )
                if result.get("success"):
                    layers_created.append("drone")
                    self.logger.info(f"Layer 3: SL2025 drone injected ({os.path.basename(drone_sample)})")
            
            # Apply track settings for ambience
            self._configure_ambience_track(track_index)
            
            return {
                "success": len(layers_created) > 0,
                "message": f"Intro ambience created with {len(layers_created)} layers",
                "layers": layers_created,
                "configuration": configuration.to_dict(),
                "track_index": track_index,
                "duration_bars": duration_bars
            }
            
        except Exception as e:
            self.logger.error(f"Failed to create intro ambience: {e}")
            return {
                "success": False,
                "message": f"Failed to create intro ambience: {str(e)}",
                "layers": layers_created,
                "track_index": track_index
            }
    
    def create_outro_fade(self, track_index: int, start_bar: float, duration_bars: int = 16) -> Dict[str, Any]:
        """
        Create gradual outro fade with ambience elements.
        
        Creates a smooth outro with:
        - Gradual volume fade automation
        - Wash effect for transition
        - Subtle rain return
        - Drone fade-out
        
        Args:
            track_index: Index of the target track for outro
            start_bar: Starting bar position for the outro
            duration_bars: Duration of the outro fade in bars (default 16)
            
        Returns:
            Dictionary with creation status and fade details
        """
        self.logger.info(f"Creating outro fade: track={track_index}, start={start_bar}, duration={duration_bars} bars")
        
        layers_created = []
        end_bar = start_bar + duration_bars
        
        try:
            # Layer 1: Wash effect for transition
            if os.path.exists(self.ambience_samples["wash"]):
                result = self._inject_sample_at_position(
                    track_index,
                    self.ambience_samples["wash"],
                    start_bar,
                    4.0
                )
                if result.get("success"):
                    layers_created.append("wash_transition")
                    self.logger.info("Outro: Wash transition injected")
            
            # Layer 2: Rain ambience fading in
            if os.path.exists(self.ambience_samples["rain"]):
                result = self._inject_sample_at_position(
                    track_index,
                    self.ambience_samples["rain"],
                    start_bar + 4,
                    duration_bars - 4
                )
                if result.get("success"):
                    layers_created.append("rain_return")
                    self.logger.info("Outro: Rain ambience returned")
            
            # Layer 3: Drone for final atmosphere
            drone_sample = self._select_random_drone()
            if drone_sample and os.path.exists(drone_sample):
                result = self._inject_sample_at_position(
                    track_index,
                    drone_sample,
                    start_bar + 8,
                    duration_bars - 8
                )
                if result.get("success"):
                    layers_created.append("drone_fade")
                    self.logger.info("Outro: Drone layer injected")
            
            # Apply fade automation
            self._apply_fade_automation(track_index, start_bar, end_bar)
            
            return {
                "success": len(layers_created) > 0,
                "message": f"Outro fade created with {len(layers_created)} layers",
                "layers": layers_created,
                "track_index": track_index,
                "start_bar": start_bar,
                "end_bar": end_bar,
                "duration_bars": duration_bars
            }
            
        except Exception as e:
            self.logger.error(f"Failed to create outro fade: {e}")
            return {
                "success": False,
                "message": f"Failed to create outro fade: {str(e)}",
                "track_index": track_index,
                "start_bar": start_bar
            }
    
    def add_texture_layers(self, track_indices: List[int], sections: List[str]) -> Dict[str, Any]:
        """
        Add texture layers to specific song sections.
        
        Adds ambience textures to sections like:
        - "intro": Sparse rain and drones
        - "verse": Minimal wash
        - "chorus": Full texture stack
        - "bridge": Atmospheric drones
        - "build": Rising textures
        
        Args:
            track_indices: List of track indices for texture placement
            sections: List of section names (intro, verse, chorus, bridge, build)
            
        Returns:
            Dictionary with creation status and texture details
        """
        self.logger.info(f"Adding texture layers to {len(sections)} sections on {len(track_indices)} tracks")
        
        results = []
        
        for i, section in enumerate(sections):
            track_idx = track_indices[i % len(track_indices)]
            section_result = {"section": section, "track_index": track_idx}
            
            try:
                if section == "intro":
                    # Sparse rain and subtle drone
                    if os.path.exists(self.ambience_samples["rain"]):
                        self._inject_sample_at_position(track_idx, self.ambience_samples["rain"], 0, 8)
                    section_result["layers"] = ["rain"]
                    
                elif section == "verse":
                    # Minimal wash effect
                    if os.path.exists(self.ambience_samples["wash"]):
                        self._inject_sample_at_position(track_idx, self.ambience_samples["wash"], 0, 4)
                    section_result["layers"] = ["wash"]
                    
                elif section == "chorus":
                    # Full texture stack
                    layers = []
                    if os.path.exists(self.ambience_samples["wash"]):
                        self._inject_sample_at_position(track_idx, self.ambience_samples["wash"], 0, 8)
                        layers.append("wash")
                    drone = self._select_random_drone()
                    if drone and os.path.exists(drone):
                        self._inject_sample_at_position(track_idx, drone, 4, 4)
                        layers.append("drone")
                    section_result["layers"] = layers
                    
                elif section == "bridge":
                    # Atmospheric drones
                    drone = self._select_random_drone()
                    if drone and os.path.exists(drone):
                        self._inject_sample_at_position(track_idx, drone, 0, 16)
                    section_result["layers"] = ["drone"]
                    
                elif section == "build":
                    # Rising texture with impact
                    layers = []
                    if os.path.exists(self.ambience_samples["wash"]):
                        self._inject_sample_at_position(track_idx, self.ambience_samples["wash"], 0, 8)
                        layers.append("wash")
                    if os.path.exists(self.ambience_samples["impact"]):
                        self._inject_sample_at_position(track_idx, self.ambience_samples["impact"], 7, 1)
                        layers.append("impact")
                    section_result["layers"] = layers
                    
                section_result["success"] = True
                results.append(section_result)
                
            except Exception as e:
                self.logger.error(f"Failed to add texture to {section}: {e}")
                section_result["success"] = False
                section_result["error"] = str(e)
                results.append(section_result)
        
        successful = [r for r in results if r.get("success")]
        
        return {
            "success": len(successful) > 0,
            "message": f"Texture layers added to {len(successful)}/{len(sections)} sections",
            "sections": results,
            "total_sections": len(sections),
            "successful_sections": len(successful)
        }
    
    def place_drones(self, track_index: int, drone_samples: List[str], 
                     build_positions: List[float]) -> Dict[str, Any]:
        """
        Place drones at build positions for tension creation.
        
        Places drones strategically at build points with:
        - Gradual volume increase
        - Filter sweep automation
        - Layered drone stacking
        
        Args:
            track_index: Index of the target track
            drone_samples: List of drone sample file paths
            build_positions: List of bar positions for drone placement
            
        Returns:
            Dictionary with placement status and drone details
        """
        self.logger.info(f"Placing {len(drone_samples)} drones at {len(build_positions)} build positions")
        
        drones_placed = []
        
        try:
            for i, position in enumerate(build_positions):
                # Cycle through drone samples
                drone_idx = i % len(drone_samples) if drone_samples else 0
                drone_path = drone_samples[drone_idx] if drone_samples else self._select_random_drone()
                
                if not drone_path or not os.path.exists(drone_path):
                    continue
                
                # Create drone layer with increasing intensity
                intensity = 0.4 + (i * 0.15)  # Gradually increase
                duration = 4.0 + (i * 2.0)  # Longer drones later
                
                result = self._inject_sample_at_position(
                    track_index,
                    drone_path,
                    position,
                    duration,
                    volume=intensity
                )
                
                if result.get("success"):
                    drones_placed.append({
                        "position": position,
                        "sample": os.path.basename(drone_path),
                        "intensity": intensity,
                        "duration": duration
                    })
            
            # Configure track for drone ambience
            self._configure_drone_track(track_index)
            
            return {
                "success": len(drones_placed) > 0,
                "message": f"Placed {len(drones_placed)} drones at build positions",
                "drones": drones_placed,
                "track_index": track_index,
                "build_positions": build_positions
            }
            
        except Exception as e:
            self.logger.error(f"Failed to place drones: {e}")
            return {
                "success": False,
                "message": f"Failed to place drones: {str(e)}",
                "track_index": track_index
            }
    
    # =========================================================================
    # Helper Methods
    # =========================================================================
    
    def _select_random_drone(self) -> Optional[str]:
        """Select a random SL2025 drone sample that exists."""
        import random
        available = [d for d in self.sl2025_drones if os.path.exists(d)]
        return random.choice(available) if available else None
    
    def _inject_sample_at_position(self, track_index: int, sample_path: str,
                                    start_bar: float, duration_bars: float,
                                    volume: float = 1.0) -> Dict[str, Any]:
        """
        Inject a sample at a specific position in the arrangement.
        
        Args:
            track_index: Target track index
            sample_path: Path to audio sample
            start_bar: Start position in bars
            duration_bars: Duration in bars
            volume: Volume multiplier (0.0-1.0)
            
        Returns:
            Result dictionary from injection
        """
        try:
            # Use the live_bridge's injection method if available
            if hasattr(self.live_bridge, 'inject_samples_round_robin'):
                result = self.live_bridge.inject_samples_round_robin(
                    track_index,
                    [sample_path],
                    [start_bar]
                )
                return result
            elif hasattr(self.live_bridge, 'create_arrangement_audio_clip'):
                result = self.live_bridge.create_arrangement_audio_clip(
                    track_index,
                    sample_path,
                    start_bar,
                    duration_bars
                )
                return result
            else:
                # Manual injection via track API
                track = self.live_bridge.song.tracks[track_index]
                if hasattr(track, 'insert_clip'):
                    clip = track.insert_clip(sample_path, start_bar, duration_bars)
                    return {"success": True, "clip": clip}
                elif hasattr(track, 'create_clip'):
                    clip = track.create_clip(start_bar, duration_bars)
                    if clip and hasattr(clip, 'add_sample'):
                        clip.add_sample(sample_path)
                    return {"success": True, "clip": clip}
                else:
                    return {"success": False, "message": "Track doesn't support clip insertion"}
                    
        except Exception as e:
            self.logger.error(f"Failed to inject sample: {e}")
            return {"success": False, "message": str(e)}
    
    def _configure_ambience_track(self, track_index: int) -> None:
        """Configure track settings for ambience."""
        try:
            # Set lower volume for ambience
            if hasattr(self.live_bridge, 'set_track_volume'):
                self.live_bridge.set_track_volume(track_index, 0.65)
            
            # Add EQ Eight for ambience shaping
            if hasattr(self.live_bridge, 'apply_eq_preset'):
                self.live_bridge.apply_eq_preset(track_index, "low_cut")
            
            # Add reverb
            if hasattr(self.live_bridge, 'insert_device'):
                self.live_bridge.insert_device(track_index, "Reverb")
                
        except Exception as e:
            self.logger.warning(f"Failed to configure ambience track: {e}")
    
    def _configure_drone_track(self, track_index: int) -> None:
        """Configure track settings for drone ambience."""
        try:
            # Set appropriate volume for drones
            if hasattr(self.live_bridge, 'set_track_volume'):
                self.live_bridge.set_track_volume(track_index, 0.55)
            
            # Add filter for drone shaping
            if hasattr(self.live_bridge, 'insert_device'):
                self.live_bridge.insert_device(track_index, "Auto Filter")
                
        except Exception as e:
            self.logger.warning(f"Failed to configure drone track: {e}")
    
    def _apply_fade_automation(self, track_index: int, start_bar: float, end_bar: float) -> None:
        """Apply volume fade automation for outro."""
        try:
            if hasattr(self.live_bridge, 'add_parameter_automation'):
                # Create fade-out automation points
                points = [
                    [start_bar * 4, 0.8],      # Start at bar position
                    [end_bar * 4, 0.0]          # Fade to silence
                ]
                self.live_bridge.add_parameter_automation(
                    track_index,
                    "volume",
                    points
                )
        except Exception as e:
            self.logger.warning(f"Failed to apply fade automation: {e}")


# =============================================================================
# Convenience Functions
# =============================================================================

def create_intro_ambience(live_bridge, track_index: int, duration_bars: int = 24) -> Dict[str, Any]:
    """Convenience function to create intro ambience."""
    generator = AmbienceGenerator(live_bridge)
    return generator.create_intro_ambience(track_index, duration_bars)


def create_outro_fade(live_bridge, track_index: int, start_bar: float, duration_bars: int = 16) -> Dict[str, Any]:
    """Convenience function to create outro fade."""
    generator = AmbienceGenerator(live_bridge)
    return generator.create_outro_fade(track_index, start_bar, duration_bars)


# =============================================================================
# Integration with live_bridge
# =============================================================================

def inject_ambience_to_track(live_bridge, track_index: int, ambience_type: str = "intro",
                                duration_bars: int = 24) -> Dict[str, Any]:
    """
    High-level function to inject ambience into a track.
    
    Args:
        live_bridge: AbletonLiveBridge instance
        track_index: Target track index
        ambience_type: Type of ambience ("intro", "outro", "bridge", "verse")
        duration_bars: Duration in bars
        
    Returns:
        Result dictionary
    """
    generator = AmbienceGenerator(live_bridge)
    
    if ambience_type == "intro":
        return generator.create_intro_ambience(track_index, duration_bars)
    elif ambience_type == "outro":
        return generator.create_outro_fade(track_index, 0, duration_bars)
    elif ambience_type == "verse":
        return generator.add_texture_layers([track_index], ["verse"])
    elif ambience_type == "chorus":
        return generator.add_texture_layers([track_index], ["chorus"])
    elif ambience_type == "bridge":
        return generator.add_texture_layers([track_index], ["bridge"])
    else:
        return {"success": False, "message": f"Unknown ambience type: {ambience_type}"}
