"""
Arrangement Engine - Arrangement View and Automation Engine

Este módulo proporciona herramientas avanzadas para trabajar con Arrangement View
en Ableton Live, incluyendo construcción de estructuras, automatización de parámetros,
creación de efectos FX y procesamiento de samples.

Autor: AbletonMCP_AI
"""
import logging
import random
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any, Tuple, Union
from pathlib import Path
import os
import math

logger = logging.getLogger("ArrangementEngine")


# =============================================================================
# CONSTANTES Y CONFIGURACIONES
# =============================================================================

# Estructuras de arrangement predefinidas
ARRANGEMENT_STRUCTURES = {
    "intro_build_drop_break_outro": [
        ("intro", 8),
        ("build", 8),
        ("drop", 16),
        ("break", 8),
        ("drop2", 16),
        ("outro", 8),
    ],
    "intro_drop_break_outro": [
        ("intro", 8),
        ("drop", 16),
        ("break", 8),
        ("outro", 8),
    ],
    "extended": [
        ("intro", 16),
        ("build", 8),
        ("drop", 16),
        ("break1", 8),
        ("build2", 8),
        ("drop2", 16),
        ("break2", 8),
        ("peak", 8),
        ("outro", 16),
    ],
}

# Configuraciones de automatización por defecto
DEFAULT_FILTER_FREQ_START = 200.0
DEFAULT_FILTER_FREQ_END = 20000.0
DEFAULT_REVERB_WET_START = 0.0
DEFAULT_REVERB_WET_END = 0.5
DEFAULT_VOLUME_START = 0.0
DEFAULT_VOLUME_END = 0.85
DEFAULT_DELAY_FEEDBACK_START = 0.1
DEFAULT_DELAY_FEEDBACK_END = 0.6

# Tipos de secciones y sus niveles de energía
SECTION_ENERGY_LEVELS = {
    "intro": 0.2,
    "build": 0.7,
    "drop": 1.0,
    "break": 0.3,
    "break1": 0.3,
    "break2": 0.4,
    "drop2": 1.0,
    "outro": 0.15,
    "build2": 0.75,
    "peak": 1.0,
}


# =============================================================================
# CLASES DE DATOS
# =============================================================================

@dataclass
class SectionMarker:
    """Representa un marcador de sección en el arrangement."""
    name: str
    start_bar: int
    end_bar: int
    color: int = 0
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "start_bar": self.start_bar,
            "end_bar": self.end_bar,
            "color": self.color,
        }


@dataclass
class AutomationPoint:
    """Punto de automatización (tiempo, valor)."""
    time: float  # En beats
    value: float
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "time": self.time,
            "value": self.value,
        }


@dataclass
class AutomationEnvelope:
    """Envelope de automatización completo."""
    parameter_name: str
    device_name: str
    points: List[AutomationPoint] = field(default_factory=list)
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "parameter_name": self.parameter_name,
            "device_name": self.device_name,
            "points": [p.to_dict() for p in self.points],
        }


@dataclass
class ArrangementClip:
    """Representa un clip en el Arrangement View."""
    name: str
    track_index: int
    start_time: float  # En beats
    duration: float
    is_audio: bool = False
    sample_path: str = ""
    notes: List[Dict[str, Any]] = field(default_factory=list)
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "track_index": self.track_index,
            "start_time": self.start_time,
            "duration": self.duration,
            "is_audio": self.is_audio,
            "sample_path": self.sample_path,
            "notes": self.notes,
        }


@dataclass
class ArrangementSection:
    """Sección completa del arrangement con clips y automatizaciones."""
    name: str
    start_bar: int
    bars: int
    clips: List[ArrangementClip] = field(default_factory=list)
    automations: List[AutomationEnvelope] = field(default_factory=list)
    energy_level: float = 0.5
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "start_bar": self.start_bar,
            "bars": self.bars,
            "clips": [c.to_dict() for c in self.clips],
            "automations": [a.to_dict() for a in self.automations],
            "energy_level": self.energy_level,
        }


@dataclass
class ArrangementConfig:
    """Configuración completa del arrangement."""
    total_bars: int
    sections: List[ArrangementSection] = field(default_factory=list)
    markers: List[SectionMarker] = field(default_factory=list)
    tempo: float = 95.0
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "total_bars": self.total_bars,
            "sections": [s.to_dict() for s in self.sections],
            "markers": [m.to_dict() for m in self.markers],
            "tempo": self.tempo,
        }


# =============================================================================
# CLASE 1: ARRANGEMENT BUILDER (T021-T025)
# =============================================================================

class ArrangementBuilder:
    """
    Constructor de estructuras de Arrangement View.
    
    Crea estructuras de canción completas (Intro→Build→Drop→Break→Outro)
    y gestiona la transición entre Session View y Arrangement View.
    """
    
    def __init__(self):
        self._config: Optional[ArrangementConfig] = None
        self._sections: List[ArrangementSection] = []
        self._markers: List[SectionMarker] = []
        
    def build_arrangement_structure(self, song_config: Dict[str, Any]) -> ArrangementConfig:
        """
        T021: Crea estructura completa Intro→Build→Drop→Break→Outro.
        
        Args:
            song_config: Configuración de canción con BPM, estructura, etc.
            
        Returns:
            ArrangementConfig con toda la estructura
        """
        structure_name = song_config.get("structure", "standard")
        bpm = song_config.get("bpm", 95.0)
        
        # Obtener configuración de estructura
        if structure_name in ARRANGEMENT_STRUCTURES:
            structure = ARRANGEMENT_STRUCTURES[structure_name]
        else:
            structure = ARRANGEMENT_STRUCTURES["intro_build_drop_break_outro"]
        
        total_bars = sum(bars for _, bars in structure)
        
        # Crear secciones
        current_bar = 0
        sections = []
        markers = []
        
        for section_name, bars in structure:
            energy = SECTION_ENERGY_LEVELS.get(section_name, 0.5)
            
            section = ArrangementSection(
                name=section_name,
                start_bar=current_bar,
                bars=bars,
                energy_level=energy,
            )
            sections.append(section)
            
            # Crear marcador
            marker = SectionMarker(
                name=section_name.upper(),
                start_bar=current_bar,
                end_bar=current_bar + bars,
                color=self._get_section_color(section_name),
            )
            markers.append(marker)
            
            current_bar += bars
        
        config = ArrangementConfig(
            total_bars=total_bars,
            sections=sections,
            markers=markers,
            tempo=bpm,
        )
        
        self._config = config
        self._sections = sections
        self._markers = markers
        
        logger.info("Estructura de arrangement creada: %d compases, %d secciones",
                   total_bars, len(sections))
        
        return config
    
    def create_section_marker(self, name: str, start_bar: int) -> SectionMarker:
        """
        T022: Crea un marcador de sección.
        
        Args:
            name: Nombre del marcador
            start_bar: Compás inicial
            
        Returns:
            SectionMarker creado
        """
        # Detectar duración basada en nombre de sección
        default_bars = {
            "intro": 8, "build": 8, "drop": 16, "break": 8,
            "outro": 8, "peak": 8,
        }
        bars = default_bars.get(name.lower(), 8)
        
        marker = SectionMarker(
            name=name.upper(),
            start_bar=start_bar,
            end_bar=start_bar + bars,
            color=self._get_section_color(name),
        )
        
        self._markers.append(marker)
        logger.info("Marcador creado: %s en compás %d", name, start_bar)
        
        return marker
    
    def duplicate_clips_to_arrangement(
        self,
        session_clips: List[Dict[str, Any]],
        arrangement_positions: List[Dict[str, Any]]
    ) -> List[ArrangementClip]:
        """
        T023: Copia clips de Session View a Arrangement View.
        
        Args:
            session_clips: Lista de clips de Session View
            arrangement_positions: Posiciones donde colocar cada clip
            
        Returns:
            Lista de ArrangementClip creados
        """
        arrangement_clips = []
        
        for i, clip_info in enumerate(session_clips):
            if i >= len(arrangement_positions):
                break
            
            pos = arrangement_positions[i]
            
            arrangement_clip = ArrangementClip(
                name=clip_info.get("name", f"Clip {i}"),
                track_index=pos.get("track_index", clip_info.get("track_index", 0)),
                start_time=pos.get("start_time", pos.get("start_bar", 0) * 4.0),
                duration=clip_info.get("duration", 4.0),
                is_audio=clip_info.get("is_audio", False),
                sample_path=clip_info.get("sample_path", ""),
                notes=clip_info.get("notes", []),
            )
            
            arrangement_clips.append(arrangement_clip)
            
            # Añadir a la sección correspondiente
            start_bar = int(arrangement_clip.start_time / 4.0)
            for section in self._sections:
                if section.start_bar <= start_bar < section.start_bar + section.bars:
                    section.clips.append(arrangement_clip)
                    break
        
        logger.info("%d clips duplicados a Arrangement View", len(arrangement_clips))
        return arrangement_clips
    
    def create_arrangement_midi_clip(
        self,
        track_index: int,
        start_time: float,
        length: float,
        notes: List[Dict[str, Any]]
    ) -> ArrangementClip:
        """
        T024: Crea un clip MIDI en Arrangement View.
        
        Args:
            track_index: Índice de la pista
            start_time: Tiempo de inicio en beats
            length: Duración en beats
            notes: Lista de notas MIDI
            
        Returns:
            ArrangementClip creado
        """
        clip = ArrangementClip(
            name=f"MIDI Clip - Track {track_index}",
            track_index=track_index,
            start_time=start_time,
            duration=length,
            is_audio=False,
            notes=notes,
        )
        
        # Añadir a sección correspondiente
        start_bar = int(start_time / 4.0)
        for section in self._sections:
            if section.start_bar <= start_bar < section.start_bar + section.bars:
                section.clips.append(clip)
                break
        
        logger.info("Clip MIDI creado: track %d, %d notas", track_index, len(notes))
        return clip
    
    def create_arrangement_audio_clip(
        self,
        track_index: int,
        sample_path: str,
        start_time: float,
        length: float
    ) -> ArrangementClip:
        """
        T025: Crea un clip de audio en Arrangement View.
        
        Args:
            track_index: Índice de la pista
            sample_path: Ruta al archivo de audio
            start_time: Tiempo de inicio en beats
            length: Duración en beats
            
        Returns:
            ArrangementClip creado
        """
        clip = ArrangementClip(
            name=os.path.basename(sample_path) if sample_path else "Audio Clip",
            track_index=track_index,
            start_time=start_time,
            duration=length,
            is_audio=True,
            sample_path=sample_path,
        )
        
        # Añadir a sección correspondiente
        start_bar = int(start_time / 4.0)
        for section in self._sections:
            if section.start_bar <= start_bar < section.start_bar + section.bars:
                section.clips.append(clip)
                break
        
        logger.info("Clip de audio creado: track %d, %s", track_index, os.path.basename(sample_path))
        return clip
    
    def fill_arrangement_with_song(self, song_config: Dict[str, Any]) -> ArrangementConfig:
        """
        Pipeline completo: crea estructura y llena con clips desde Session View.
        
        Args:
            song_config: Configuración completa de la canción
            
        Returns:
            ArrangementConfig final
        """
        # 1. Crear estructura base
        config = self.build_arrangement_structure(song_config)
        
        # 2. Procesar tracks de la configuración
        tracks = song_config.get("tracks", [])
        
        for track_idx, track in enumerate(tracks):
            clips = track.get("clips", [])
            
            for clip in clips:
                start_time = clip.get("start_time", 0.0)
                duration = clip.get("duration", 4.0)
                notes = clip.get("notes", [])
                sample_path = clip.get("sample_path", "")
                
                if sample_path:
                    # Es un clip de audio
                    self.create_arrangement_audio_clip(
                        track_index=track_idx,
                        sample_path=sample_path,
                        start_time=start_time,
                        length=duration
                    )
                elif notes:
                    # Es un clip MIDI
                    self.create_arrangement_midi_clip(
                        track_index=track_idx,
                        start_time=start_time,
                        length=duration,
                        notes=notes
                    )
        
        logger.info("Pipeline completado: arrangement lleno con %d tracks", len(tracks))
        return config
    
    def build_sectioned_arrangement(
        self,
        song_structure: Dict[str, Any],
        sample_map: Dict[str, List[str]],
        variation_config: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        Build a sectioned arrangement with sample variations for expansive production.
        
        This method creates a complete arrangement by mapping samples to different song
        sections (intro, verse, chorus, etc.) and applying variation strategies per role.
        Integrates with SectionSampleMapper, VariationController, and MultiSampleInjector.
        
        Args:
            song_structure: Dictionary defining song sections with their properties.
                Example: {
                    "sections": [
                        {"type": "intro", "start_bar": 0, "duration_bars": 8, 
                         "elements": ["drums", "bass"], "energy": 0.3},
                        {"type": "verse", "start_bar": 8, "duration_bars": 16,
                         "elements": ["drums", "bass", "chords"], "energy": 0.6},
                        {"type": "chorus", "start_bar": 24, "duration_bars": 16,
                         "elements": ["drums", "bass", "chords", "melody"], "energy": 1.0}
                    ],
                    "bpm": 95,
                    "key": "Am",
                    "style": "reggaeton"
                }
            sample_map: Dictionary mapping roles to lists of available sample paths.
                Example: {
                    "drums": ["/path/kick1.wav", "/path/kick2.wav"],
                    "bass": ["/path/bass1.wav", "/path/bass2.wav"],
                    "chords": ["/path/chord1.wav"],
                    "melody": ["/path/melody1.wav", "/path/melody2.wav"]
                }
            variation_config: Dictionary defining variation strategies per role.
                Example: {
                    "drums": {"strategy": "round_robin", "cycle_every": "bar"},
                    "bass": {"strategy": "layered", "layers": 2},
                    "chords": {"strategy": "single"},
                    "melody": {"strategy": "round_robin", "cycle_every": "section"},
                    "default": {"strategy": "round_robin"}
                }
                
        Returns:
            Dictionary containing:
            - status: "success" or "error"
            - arrangement: ArrangementConfig object (as dict)
            - clips_created: List of created ArrangementClip objects (as dicts)
            - sections_processed: List of section names processed
            - samples_used: Dictionary mapping sections to samples used per role
            - variations_applied: Dictionary mapping roles to variation strategies used
        """
        # Import new engines (lazy import to avoid circular dependencies)
        try:
            from section_sample_mapper import SectionSampleMapper
            from variation_controller import VariationController
            from multi_sample_injector import MultiSampleInjector
            engines_available = True
        except ImportError as e:
            engines_available = False
            logger.warning("New engines not available: %s. Using fallback logic.", str(e))
        
        clips_created = []
        sections_processed = []
        samples_used = {}
        variations_applied = {}
        
        try:
            # Extract song configuration
            sections = song_structure.get("sections", [])
            bpm = song_structure.get("bpm", 95.0)
            key = song_structure.get("key", "Am")
            style = song_structure.get("style", "reggaeton")
            
            if not sections:
                return {
                    "status": "error",
                    "message": "No sections defined in song_structure",
                    "clips_created": [],
                    "sections_processed": [],
                    "samples_used": {},
                    "variations_applied": {}
                }
            
            # Calculate total bars
            total_bars = 0
            for section in sections:
                end_bar = section.get("start_bar", 0) + section.get("duration_bars", 8)
                total_bars = max(total_bars, end_bar)
            
            # Create arrangement configuration
            arrangement_config = ArrangementConfig(
                total_bars=total_bars,
                sections=[],
                markers=[],
                tempo=bpm
            )
            self._config = arrangement_config
            
            # Initialize engines if available
            if engines_available:
                sample_mapper = SectionSampleMapper()
                variation_controller = VariationController()
                injector = MultiSampleInjector()
            else:
                sample_mapper = None
                variation_controller = None
                injector = None
            
            # Process each section
            for section_idx, section in enumerate(sections):
                section_type = section.get("type", "unknown")
                start_bar = section.get("start_bar", 0)
                duration_bars = section.get("duration_bars", 8)
                elements = section.get("elements", [])
                energy_level = section.get("energy", 0.5)
                
                # Create ArrangementSection
                arr_section = ArrangementSection(
                    name=section_type,
                    start_bar=start_bar,
                    bars=duration_bars,
                    energy_level=energy_level
                )
                arrangement_config.sections.append(arr_section)
                
                # Create section marker
                marker = SectionMarker(
                    name=section_type.upper(),
                    start_bar=start_bar,
                    end_bar=start_bar + duration_bars,
                    color=self._get_section_color(section_type)
                )
                arrangement_config.markers.append(marker)
                
                sections_processed.append(section_type)
                samples_used[section_type] = {}
                
                # Process each element (role) in the section
                for role in elements:
                    if role not in sample_map or not sample_map[role]:
                        logger.warning("No samples available for role '%s' in section '%s'", 
                                     role, section_type)
                        continue
                    
                    # Get available samples for this role
                    available_samples = sample_map[role]
                    
                    # Determine variation strategy for this role
                    role_variation = variation_config.get(role, variation_config.get("default", {}))
                    strategy = role_variation.get("strategy", "round_robin")
                    variations_applied[role] = strategy
                    
                    # Select samples based on strategy
                    if engines_available and sample_mapper:
                        # Use SectionSampleMapper for intelligent mapping
                        mapped_samples = sample_mapper.map_samples_to_section(
                            role=role,
                            section_type=section_type,
                            section_idx=section_idx,
                            available_samples=available_samples,
                            duration_bars=duration_bars
                        )
                    else:
                        # Fallback: simple round-robin selection
                        mapped_samples = self._fallback_sample_mapping(
                            role=role,
                            section_type=section_type,
                            section_idx=section_idx,
                            available_samples=available_samples,
                            duration_bars=duration_bars,
                            strategy=strategy,
                            variation_config=role_variation
                        )
                    
                    # Track samples used for this section
                    samples_used[section_type][role] = [s.get("path", s) if isinstance(s, dict) else s 
                                                          for s in mapped_samples]
                    
                    # Create clips for each mapped sample
                    for sample_info in mapped_samples:
                        if isinstance(sample_info, dict):
                            sample_path = sample_info.get("path", "")
                            clip_start_bar = start_bar + sample_info.get("offset_bars", 0)
                            clip_duration = sample_info.get("duration_bars", duration_bars) * 4.0
                        else:
                            sample_path = sample_info
                            clip_start_bar = start_bar
                            clip_duration = duration_bars * 4.0
                        
                        if not sample_path:
                            continue
                        
                        # Calculate track index based on role
                        track_index = self._get_track_index_for_role(role, section_idx)
                        
                        # Create the clip
                        clip = self.create_arrangement_audio_clip(
                            track_index=track_index,
                            sample_path=sample_path,
                            start_time=clip_start_bar * 4.0,  # Convert to beats
                            length=clip_duration
                        )
                        
                        clips_created.append(clip)
                        arr_section.clips.append(clip)
                        
                        logger.info("Created clip: %s for %s in %s at bar %d", 
                                  os.path.basename(sample_path), role, section_type, clip_start_bar)
                
                # Apply variation strategy if using VariationController
                if engines_available and variation_controller:
                    for role in elements:
                        if role in variations_applied:
                            variation_controller.apply_variation(
                                section=arr_section,
                                role=role,
                                strategy=variations_applied[role],
                                config=variation_config.get(role, {})
                            )
            
            # Store sections internally
            self._sections = arrangement_config.sections
            self._markers = arrangement_config.markers
            
            logger.info("Sectioned arrangement built: %d sections, %d clips", 
                       len(sections_processed), len(clips_created))
            
            return {
                "status": "success",
                "arrangement": arrangement_config.to_dict(),
                "clips_created": [clip.to_dict() for clip in clips_created],
                "sections_processed": sections_processed,
                "samples_used": samples_used,
                "variations_applied": variations_applied,
                "total_bars": total_bars,
                "bpm": bpm,
                "key": key,
                "style": style,
                "engines_used": {
                    "section_sample_mapper": engines_available,
                    "variation_controller": engines_available,
                    "multi_sample_injector": engines_available
                }
            }
            
        except Exception as e:
            logger.error("Error building sectioned arrangement: %s", str(e), exc_info=True)
            return {
                "status": "error",
                "message": str(e),
                "clips_created": [clip.to_dict() for clip in clips_created],
                "sections_processed": sections_processed,
                "samples_used": samples_used,
                "variations_applied": variations_applied
            }
    
    def _fallback_sample_mapping(
        self,
        role: str,
        section_type: str,
        section_idx: int,
        available_samples: List[str],
        duration_bars: int,
        strategy: str,
        variation_config: Dict[str, Any]
    ) -> List[Dict[str, Any]]:
        """
        Fallback sample mapping when SectionSampleMapper is not available.
        
        Implements basic variation strategies:
        - round_robin: Cycles through samples
        - layered: Uses multiple samples simultaneously
        - single: Uses one sample for entire section
        - random: Randomly selects samples
        """
        mapped = []
        num_samples = len(available_samples)
        
        if strategy == "single" or num_samples == 1:
            # Use first sample for entire section
            mapped.append({
                "path": available_samples[0],
                "offset_bars": 0,
                "duration_bars": duration_bars
            })
            
        elif strategy == "round_robin":
            cycle_every = variation_config.get("cycle_every", "section")
            
            if cycle_every == "section":
                # One sample per section, cycling through sections
                sample_idx = section_idx % num_samples
                mapped.append({
                    "path": available_samples[sample_idx],
                    "offset_bars": 0,
                    "duration_bars": duration_bars
                })
                
            elif cycle_every == "bar":
                # Different sample each bar
                for bar in range(duration_bars):
                    sample_idx = (section_idx * duration_bars + bar) % num_samples
                    mapped.append({
                        "path": available_samples[sample_idx],
                        "offset_bars": bar,
                        "duration_bars": 1
                    })
                    
            elif cycle_every == "half":
                # Different sample each half
                half_bars = max(1, duration_bars // 2)
                for i in range(0, duration_bars, half_bars):
                    sample_idx = (section_idx * 2 + i // half_bars) % num_samples
                    mapped.append({
                        "path": available_samples[sample_idx],
                        "offset_bars": i,
                        "duration_bars": min(half_bars, duration_bars - i)
                    })
                    
        elif strategy == "layered":
            layers = min(variation_config.get("layers", 2), num_samples)
            for i in range(layers):
                mapped.append({
                    "path": available_samples[i % num_samples],
                    "offset_bars": 0,
                    "duration_bars": duration_bars
                })
                
        elif strategy == "random":
            # Randomly select samples
            num_to_use = variation_config.get("count", 1)
            for i in range(min(num_to_use, duration_bars)):
                import random
                sample_idx = random.randint(0, num_samples - 1)
                mapped.append({
                    "path": available_samples[sample_idx],
                    "offset_bars": i,
                    "duration_bars": 1
                })
        else:
            # Default to single sample
            mapped.append({
                "path": available_samples[0],
                "offset_bars": 0,
                "duration_bars": duration_bars
            })
        
        return mapped
    
    def _get_track_index_for_role(self, role: str, section_idx: int) -> int:
        """
        Map a role to a track index.
        
        Roles are assigned to consistent track indices:
        - drums: track 0
        - bass: track 1
        - chords: track 2
        - melody: track 3
        - fx: track 4
        - perc: track 5
        - Additional roles: track 6+
        """
        role_to_track = {
            "drums": 0,
            "kick": 0,
            "snare": 0,
            "bass": 1,
            "chords": 2,
            "melody": 3,
            "fx": 4,
            "perc": 5,
            "percussion": 5,
            "pad": 6,
            "synth": 7,
            "lead": 8,
            "vocal": 9,
        }
        
        base_track = role_to_track.get(role.lower(), 10)
        # Add section offset for layering if needed
        return base_track
    
    def _get_section_color(self, section_name: str) -> int:
        """Retorna color para una sección según su tipo."""
        colors = {
            "intro": 1,      # Azul
            "build": 3,      # Naranja
            "drop": 5,       # Rojo
            "break": 2,      # Verde
            "break1": 2,
            "break2": 2,
            "drop2": 5,
            "outro": 6,      # Púrpura
            "peak": 4,       # Amarillo
        }
        return colors.get(section_name.lower(), 0)


# =============================================================================
# CLASE 2: AUTOMATION ENGINE (T026-T030)
# =============================================================================

class AutomationEngine:
    """
    Motor de automatización para parámetros de devices y mezcla.
    
    Crea envelopes de automatización para efectos comunes como
    filtros, reverb, volumen, delay y envíos.
    """
    
    def __init__(self):
        self._envelopes: List[AutomationEnvelope] = []
    
    def automate_filter(
        self,
        track_index: int,
        start_bar: int,
        end_bar: int,
        start_freq: float = DEFAULT_FILTER_FREQ_START,
        end_freq: float = DEFAULT_FILTER_FREQ_END,
        curve: str = "linear"
    ) -> AutomationEnvelope:
        """
        T026: Automatización de cutoff de AutoFilter (sweep).
        
        Args:
            track_index: Índice de la pista
            start_bar: Compás inicial
            end_bar: Compás final
            start_freq: Frecuencia inicial en Hz
            end_freq: Frecuencia final en Hz
            curve: Tipo de curva ("linear", "exponential", "logarithmic")
            
        Returns:
            AutomationEnvelope creado
        """
        start_time = start_bar * 4.0
        end_time = end_bar * 4.0
        duration = end_time - start_time
        
        points = []
        num_points = max(8, int(duration / 4))  # Un punto por compás mínimo
        
        for i in range(num_points + 1):
            t = i / num_points
            time = start_time + t * duration
            
            if curve == "exponential":
                t = t * t
            elif curve == "logarithmic":
                t = math.sqrt(t)
            
            # Interpolación logarítmica para frecuencia
            freq = start_freq * ((end_freq / start_freq) ** t)
            
            points.append(AutomationPoint(time=time, value=freq))
        
        envelope = AutomationEnvelope(
            parameter_name="Frequency",
            device_name="AutoFilter",
            points=points,
        )
        
        self._envelopes.append(envelope)
        logger.info("AutoFilter sweep: %d->%d compases, %.0f->%.0f Hz",
                   start_bar, end_bar, start_freq, end_freq)
        
        return envelope
    
    def automate_reverb(
        self,
        track_index: int,
        start_bar: int,
        end_bar: int,
        dry_wet_start: float = DEFAULT_REVERB_WET_START,
        dry_wet_end: float = DEFAULT_REVERB_WET_END,
        parameter: str = "Dry/Wet"
    ) -> AutomationEnvelope:
        """
        T027: Automatización de wet/dry de reverb.
        
        Args:
            track_index: Índice de la pista
            start_bar: Compás inicial
            end_bar: Compás final
            dry_wet_start: Valor inicial (0.0-1.0)
            dry_wet_end: Valor final (0.0-1.0)
            parameter: Nombre del parámetro a automatizar
            
        Returns:
            AutomationEnvelope creado
        """
        start_time = start_bar * 4.0
        end_time = end_bar * 4.0
        duration = end_time - start_time
        
        points = []
        num_points = max(4, int(duration / 4))
        
        for i in range(num_points + 1):
            t = i / num_points
            time = start_time + t * duration
            
            # Interpolación lineal
            value = dry_wet_start + (dry_wet_end - dry_wet_start) * t
            
            points.append(AutomationPoint(time=time, value=value))
        
        envelope = AutomationEnvelope(
            parameter_name=parameter,
            device_name="Reverb",
            points=points,
        )
        
        self._envelopes.append(envelope)
        logger.info("Reverb automation: %d->%d compases, %.2f->%.2f",
                   start_bar, end_bar, dry_wet_start, dry_wet_end)
        
        return envelope
    
    def automate_volume(
        self,
        track_index: int,
        start_bar: int,
        end_bar: int,
        start_vol: float = DEFAULT_VOLUME_START,
        end_vol: float = DEFAULT_VOLUME_END,
        fade_type: str = "in"
    ) -> AutomationEnvelope:
        """
        T028: Automatización de volumen (fade in/out).
        
        Args:
            track_index: Índice de la pista
            start_bar: Compás inicial
            end_bar: Compás final
            start_vol: Volumen inicial (0.0-1.0)
            end_vol: Volumen final (0.0-1.0)
            fade_type: "in", "out", o "crossfade"
            
        Returns:
            AutomationEnvelope creado
        """
        start_time = start_bar * 4.0
        end_time = end_bar * 4.0
        duration = end_time - start_time
        
        points = []
        num_points = max(4, int(duration / 4))
        
        for i in range(num_points + 1):
            t = i / num_points
            time = start_time + t * duration
            
            # Curva de fade más natural
            if fade_type == "in":
                t = t * t  # Curva exponencial suave
            elif fade_type == "out":
                t = math.sqrt(t)
            
            value = start_vol + (end_vol - start_vol) * t
            points.append(AutomationPoint(time=time, value=value))
        
        envelope = AutomationEnvelope(
            parameter_name="Volume",
            device_name="Mixer",
            points=points,
        )
        
        self._envelopes.append(envelope)
        logger.info("Volume fade %s: %d->%d compases, %.2f->%.2f",
                   fade_type, start_bar, end_bar, start_vol, end_vol)
        
        return envelope
    
    def automate_delay(
        self,
        track_index: int,
        start_bar: int,
        end_bar: int,
        feedback_start: float = DEFAULT_DELAY_FEEDBACK_START,
        feedback_end: float = DEFAULT_DELAY_FEEDBACK_END,
        parameter: str = "Feedback"
    ) -> AutomationEnvelope:
        """
        T029: Automatización de feedback de delay.
        
        Args:
            track_index: Índice de la pista
            start_bar: Compás inicial
            end_bar: Compás final
            feedback_start: Feedback inicial (0.0-1.0)
            feedback_end: Feedback final (0.0-1.0)
            parameter: Nombre del parámetro
            
        Returns:
            AutomationEnvelope creado
        """
        start_time = start_bar * 4.0
        end_time = end_bar * 4.0
        duration = end_time - start_time
        
        points = []
        num_points = max(4, int(duration / 4))
        
        for i in range(num_points + 1):
            t = i / num_points
            time = start_time + t * duration
            
            value = feedback_start + (feedback_end - feedback_start) * t
            points.append(AutomationPoint(time=time, value=value))
        
        envelope = AutomationEnvelope(
            parameter_name=parameter,
            device_name="Delay",
            points=points,
        )
        
        self._envelopes.append(envelope)
        logger.info("Delay feedback: %d->%d compases, %.2f->%.2f",
                   start_bar, end_bar, feedback_start, feedback_end)
        
        return envelope
    
    def automate_send(
        self,
        track_index: int,
        return_index: int,
        start_bar: int,
        end_bar: int,
        start_amount: float = 0.0,
        end_amount: float = 0.5,
        send_name: str = ""
    ) -> AutomationEnvelope:
        """
        T030: Automatización de cantidad de envío (send).
        
        Args:
            track_index: Índice de la pista
            return_index: Índice del track de retorno
            start_bar: Compás inicial
            end_bar: Compás final
            start_amount: Cantidad inicial (0.0-1.0)
            end_amount: Cantidad final (0.0-1.0)
            send_name: Nombre opcional del send
            
        Returns:
            AutomationEnvelope creado
        """
        start_time = start_bar * 4.0
        end_time = end_bar * 4.0
        duration = end_time - start_time
        
        points = []
        num_points = max(4, int(duration / 4))
        
        for i in range(num_points + 1):
            t = i / num_points
            time = start_time + t * duration
            
            value = start_amount + (end_amount - start_amount) * t
            points.append(AutomationPoint(time=time, value=value))
        
        device_name = send_name if send_name else f"Send {return_index}"
        
        envelope = AutomationEnvelope(
            parameter_name="Send Amount",
            device_name=device_name,
            points=points,
        )
        
        self._envelopes.append(envelope)
        logger.info("Send automation: %d->%d compases, %.2f->%.2f",
                   start_bar, end_bar, start_amount, end_amount)
        
        return envelope
    
    def get_all_envelopes(self) -> List[AutomationEnvelope]:
        """Retorna todos los envelopes creados."""
        return self._envelopes.copy()


# =============================================================================
# CLASE 3: FX CREATOR (T031-T035)
# =============================================================================

class FXCreator:
    """
    Creador de efectos FX para transiciones y énfasis.
    
    Genera risers, downlifters, impacts y otros efectos
    para mejorar las transiciones entre secciones.
    """
    
    def __init__(self):
        self._fx_clips: List[ArrangementClip] = []
    
    def create_riser(
        self,
        track_index: int,
        start_bar: int,
        duration: int = 8,
        intensity: float = 0.8,
        pitch_range: Tuple[int, int] = (36, 84)
    ) -> ArrangementClip:
        """
        T031: Crea un riser pre-drop (crescendo de pitch/tensión).
        
        Args:
            track_index: Índice de la pista
            start_bar: Compás inicial
            duration: Duración en compases
            intensity: Intensidad (0.0-1.0)
            pitch_range: Rango de notas MIDI (min, max)
            
        Returns:
            ArrangementClip del riser
        """
        start_time = start_bar * 4.0
        total_duration = duration * 4.0
        
        # Crear notas que suben de pitch
        notes = []
        num_notes = int(duration * 4 * 2)  # 2 notas por beat
        
        min_pitch, max_pitch = pitch_range
        
        for i in range(num_notes):
            t = i / num_notes
            time = start_time + t * total_duration
            
            # Pitch ascendente
            pitch = int(min_pitch + (max_pitch - min_pitch) * t)
            
            # Velocity ascendente para más tensión
            velocity = int(60 + 67 * t * intensity)
            
            # Duración más corta al final para staccato effect
            note_duration = 0.5 - (0.3 * t)
            
            notes.append({
                "pitch": pitch,
                "start_time": time,
                "duration": max(0.1, note_duration),
                "velocity": min(127, velocity),
            })
        
        clip = ArrangementClip(
            name=f"Riser - {duration} bars",
            track_index=track_index,
            start_time=start_time,
            duration=total_duration,
            is_audio=False,
            notes=notes,
        )
        
        self._fx_clips.append(clip)
        logger.info("Riser creado: %d compases, intensidad %.2f", duration, intensity)
        
        return clip
    
    def create_downlifter(
        self,
        track_index: int,
        start_bar: int,
        duration: int = 4,
        intensity: float = 0.7,
        pitch_range: Tuple[int, int] = (72, 36)
    ) -> ArrangementClip:
        """
        T032: Crea un downlifter post-drop (descenso de pitch/tensión).
        
        Args:
            track_index: Índice de la pista
            start_bar: Compás inicial
            duration: Duración en compases
            intensity: Intensidad (0.0-1.0)
            pitch_range: Rango de notas MIDI (start, end)
            
        Returns:
            ArrangementClip del downlifter
        """
        start_time = start_bar * 4.0
        total_duration = duration * 4.0
        
        notes = []
        num_notes = int(duration * 4)
        
        start_pitch, end_pitch = pitch_range
        
        for i in range(num_notes):
            t = i / num_notes
            time = start_time + t * total_duration
            
            # Pitch descendente
            pitch = int(start_pitch + (end_pitch - start_pitch) * t)
            
            # Velocity descendente
            velocity = int(100 - 60 * t * intensity)
            
            notes.append({
                "pitch": pitch,
                "start_time": time,
                "duration": 0.5,
                "velocity": max(1, velocity),
            })
        
        clip = ArrangementClip(
            name=f"Downlifter - {duration} bars",
            track_index=track_index,
            start_time=start_time,
            duration=total_duration,
            is_audio=False,
            notes=notes,
        )
        
        self._fx_clips.append(clip)
        logger.info("Downlifter creado: %d compases, intensidad %.2f", duration, intensity)
        
        return clip
    
    def create_impact(
        self,
        track_index: int,
        position: Union[int, float],
        intensity: float = 1.0,
        impact_type: str = "hit"
    ) -> ArrangementClip:
        """
        T033: Crea un impact FX (hit, crash, sub drop).
        
        Args:
            track_index: Índice de la pista
            position: Posición en compases (int) o beats (float)
            intensity: Intensidad del impacto (0.0-1.0)
            impact_type: Tipo de impacto ("hit", "crash", "sub_drop", "noise")
            
        Returns:
            ArrangementClip del impact
        """
        if isinstance(position, int):
            start_time = position * 4.0
        else:
            start_time = position
        
        # Configuración según tipo
        if impact_type == "hit":
            base_pitch = 36
            velocity = int(100 + 27 * intensity)
            duration = 2.0
        elif impact_type == "crash":
            base_pitch = 49
            velocity = int(80 + 47 * intensity)
            duration = 4.0
        elif impact_type == "sub_drop":
            base_pitch = 24
            velocity = int(110 + 17 * intensity)
            duration = 3.0
        else:  # noise
            base_pitch = 60
            velocity = int(90 + 37 * intensity)
            duration = 2.0
        
        notes = [{
            "pitch": base_pitch,
            "start_time": start_time,
            "duration": duration,
            "velocity": min(127, velocity),
        }]
        
        clip = ArrangementClip(
            name=f"Impact {impact_type}",
            track_index=track_index,
            start_time=start_time,
            duration=duration,
            is_audio=False,
            notes=notes,
        )
        
        self._fx_clips.append(clip)
        logger.info("Impact creado: %s en %.2f, intensidad %.2f", impact_type, position, intensity)
        
        return clip
    
    def create_silence(
        self,
        track_index: int,
        start_bar: int,
        duration: int = 1,
        fade_edges: bool = True
    ) -> ArrangementClip:
        """
        T034: Crea una barra de silencio (mute momentáneo).
        
        Args:
            track_index: Índice de la pista
            start_bar: Compás inicial
            duration: Duración en compases
            fade_edges: Si se aplican fades en los bordes
            
        Returns:
            ArrangementClip de silencio (como marcador)
        """
        start_time = start_bar * 4.0
        total_duration = duration * 4.0
        
        # El silencio se implementa como un clip vacío con metadatos
        # En la práctica, esto se usa para automatizar el volumen a -inf
        clip = ArrangementClip(
            name=f"Silence - {duration} bars",
            track_index=track_index,
            start_time=start_time,
            duration=total_duration,
            is_audio=False,
            notes=[],  # Sin notas = silencio
        )
        
        self._fx_clips.append(clip)
        logger.info("Silencio creado: %d compases desde compás %d", duration, start_bar)
        
        return clip
    
    def create_fx_automation_section(
        self,
        section_type: str,
        start_bar: int,
        duration: int,
        track_indices: Optional[List[int]] = None
    ) -> List[ArrangementClip]:
        """
        T035: Crea una sección completa de FX según el tipo.
        
        Args:
            section_type: Tipo de sección ("pre_drop", "post_drop", "transition")
            start_bar: Compás inicial
            duration: Duración en compases
            track_indices: Lista de tracks afectados (None = todos)
            
        Returns:
            Lista de ArrangementClips de FX
        """
        clips = []
        
        if track_indices is None:
            track_indices = [0, 1, 2]  # Default tracks
        
        if section_type == "pre_drop":
            # Riser en build
            for idx in track_indices[:1]:  # Solo en primer track de FX
                clip = self.create_riser(idx, start_bar, duration, intensity=0.9)
                clips.append(clip)
            
            # Impact al final
            if len(track_indices) > 1:
                impact = self.create_impact(
                    track_indices[1],
                    start_bar + duration,
                    intensity=1.0,
                    impact_type="hit"
                )
                clips.append(impact)
                
        elif section_type == "post_drop":
            # Downlifter después del drop
            for idx in track_indices[:1]:
                clip = self.create_downlifter(idx, start_bar, duration, intensity=0.6)
                clips.append(clip)
                
        elif section_type == "transition":
            # Swell hacia arriba y luego down
            half_duration = duration // 2
            
            for idx in track_indices[:1]:
                # Primera mitad: subida
                rise = self.create_riser(idx, start_bar, half_duration, intensity=0.7)
                clips.append(rise)
                
                # Segunda mitad: bajada
                down = self.create_downlifter(idx, start_bar + half_duration, half_duration, intensity=0.5)
                clips.append(down)
        
        logger.info("Sección FX '%s' creada: %d clips", section_type, len(clips))
        return clips
    
    def get_all_fx_clips(self) -> List[ArrangementClip]:
        """Retorna todos los clips FX creados."""
        return self._fx_clips.copy()


# =============================================================================
# CLASE 4: SAMPLE PROCESSOR (T036-T040)
# =============================================================================

class SampleProcessor:
    """
    Procesador avanzado de samples.
    
    Proporciona funcionalidades para resamplear, revertir, hacer slices,
    aplicar efectos granulares y crear capas ambientales.
    """
    
    def __init__(self):
        self._processed_samples: List[Dict[str, Any]] = []
    
    def resample_track(
        self,
        track_index: int,
        output_track_index: int,
        start_bar: int = 0,
        duration_bars: int = 16,
        output_name: str = "Resampled"
    ) -> Dict[str, Any]:
        """
        T036: Graba/resamplea un track a un track de audio.
        
        Args:
            track_index: Índice del track a resamplear
            output_track_index: Índice del track de salida
            start_bar: Compás de inicio
            duration_bars: Duración en compases
            output_name: Nombre del clip resultante
            
        Returns:
            Información del sample resampleado
        """
        start_time = start_bar * 4.0
        duration = duration_bars * 4.0
        
        result = {
            "source_track": track_index,
            "output_track": output_track_index,
            "start_time": start_time,
            "duration": duration,
            "name": output_name,
            "status": "configured",
            "note": "Resampling requiere renderizado en Ableton Live",
        }
        
        self._processed_samples.append(result)
        logger.info("Resample configurado: track %d -> %d (%d compases)",
                   track_index, output_track_index, duration_bars)
        
        return result
    
    def reverse_sample(
        self,
        sample_path: str,
        output_path: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        T037: Carga un sample, lo revierte y guarda nuevo archivo.
        
        Args:
            sample_path: Ruta al sample original
            output_path: Ruta de salida (None = añade _reversed)
            
        Returns:
            Información del sample revertido
        """
        if not os.path.isfile(sample_path):
            return {"error": f"Sample no encontrado: {sample_path}"}
        
        # Generar nombre de salida si no se proporciona
        if output_path is None:
            base, ext = os.path.splitext(sample_path)
            output_path = f"{base}_reversed{ext}"
        
        result = {
            "original_path": sample_path,
            "output_path": output_path,
            "status": "configured",
            "note": "Reversing requiere procesamiento de audio externo",
        }
        
        self._processed_samples.append(result)
        logger.info("Reverse configurado: %s", os.path.basename(sample_path))
        
        return result
    
    def slice_and_rearrange(
        self,
        sample_path: str,
        num_slices: int = 8,
        new_pattern: Optional[List[int]] = None
    ) -> Dict[str, Any]:
        """
        T038: Divide un sample en slices y los rearrangea.
        
        Args:
            sample_path: Ruta al sample
            num_slices: Número de slices a crear
            new_pattern: Patrón de rearrange (índices de slices)
            
        Returns:
            Información del sample procesado
        """
        if not os.path.isfile(sample_path):
            return {"error": f"Sample no encontrado: {sample_path}"}
        
        # Si no hay patrón, crear uno aleatorio
        if new_pattern is None:
            new_pattern = list(range(num_slices))
            random.shuffle(new_pattern)
        
        # Calcular puntos de slice (posiciones en beats)
        # Asumimos un sample de 4 compases por defecto
        total_beats = 16.0
        slice_duration = total_beats / num_slices
        
        slices = []
        for i in range(num_slices):
            start = i * slice_duration
            end = (i + 1) * slice_duration
            slices.append({
                "index": i,
                "start_beat": start,
                "end_beat": end,
                "duration": slice_duration,
            })
        
        # Crear nuevo orden
        rearranged = []
        for idx in new_pattern:
            if 0 <= idx < len(slices):
                rearranged.append(slices[idx].copy())
        
        result = {
            "original_path": sample_path,
            "num_slices": num_slices,
            "slices": slices,
            "new_pattern": new_pattern,
            "rearranged": rearranged,
            "status": "configured",
        }
        
        self._processed_samples.append(result)
        logger.info("Slice & rearrange: %d slices, patrón %s", num_slices, new_pattern)
        
        return result
    
    def apply_granular_effect(
        self,
        track_index: int,
        grain_size: float = 0.1,
        density: float = 0.5,
        spread: float = 0.3,
        duration_bars: int = 4
    ) -> Dict[str, Any]:
        """
        T039: Aplica efecto granular (simulado con notas MIDI).
        
        Args:
            track_index: Índice del track
            grain_size: Tamaño de grano en beats
            density: Densidad de granos (0.0-1.0)
            spread: Dispersión estéreo/pitch
            duration_bars: Duración en compases
            
        Returns:
            Información del efecto aplicado
        """
        duration = duration_bars * 4.0
        
        # Crear notas que simulan granos
        notes = []
        current_time = 0.0
        
        while current_time < duration:
            # Decidir si colocar un grano
            if random.random() < density:
                # Pitch aleatorio con spread
                base_pitch = 60
                pitch_variation = int(spread * 24 * (random.random() - 0.5))
                pitch = base_pitch + pitch_variation
                
                # Velocity aleatoria
                velocity = int(60 + 40 * random.random())
                
                notes.append({
                    "pitch": pitch,
                    "start_time": current_time,
                    "duration": grain_size,
                    "velocity": velocity,
                })
            
            # Avanzar
            current_time += grain_size * (0.5 + random.random() * 0.5)
        
        result = {
            "track_index": track_index,
            "grain_size": grain_size,
            "density": density,
            "spread": spread,
            "note_count": len(notes),
            "notes": notes,
            "status": "configured",
        }
        
        self._processed_samples.append(result)
        logger.info("Granular effect: %d notas en %d compases", len(notes), duration_bars)
        
        return result
    
    def create_ambient_layer(
        self,
        chord_progression: List[str],
        duration: int = 32,
        base_octave: int = 4,
        track_name: str = "Ambient Pad"
    ) -> Dict[str, Any]:
        """
        T040: Crea un track de pad ambiente con progresión armónica.
        
        Args:
            chord_progression: Lista de acordes (ej: ["Am", "F", "C", "G"])
            duration: Duración total en compases
            base_octave: Octava base (4 = C4)
            track_name: Nombre del track
            
        Returns:
            Configuración del pad ambiente
        """
        # Mapeo de acordes a notas MIDI
        chord_notes = {
            "Am": [9, 12, 16],      # A, C, E
            "Dm": [2, 5, 9],        # D, F, A
            "Em": [4, 7, 11],       # E, G, B
            "F": [5, 9, 12],        # F, A, C
            "G": [7, 11, 14],       # G, B, D
            "C": [0, 4, 7],         # C, E, G
            "D": [2, 6, 9],         # D, F#, A
            "E": [4, 8, 11],        # E, G#, B
            "A": [9, 13, 16],       # A, C#, E
            "Bm": [11, 14, 18],     # B, D, F#
        }
        
        base_midi = 12 * (base_octave + 1)  # C4 = 60
        
        # Calcular compases por acorde
        bars_per_chord = duration // len(chord_progression)
        
        notes = []
        current_bar = 0
        
        for chord in chord_progression:
            intervals = chord_notes.get(chord, [0, 4, 7])
            
            # Crear notas del acorde extendidas
            for bar in range(bars_per_chord):
                for beat in range(4):
                    # Notas largas para efecto pad
                    if beat == 0 or random.random() < 0.3:
                        for interval in intervals:
                            pitch = base_midi + interval
                            # Añadir variación de octava
                            if random.random() < 0.2:
                                pitch += 12
                            
                            note_time = (current_bar + bar) * 4.0 + beat
                            
                            notes.append({
                                "pitch": pitch,
                                "start_time": note_time,
                                "duration": 2.0 + random.random() * 2.0,
                                "velocity": int(50 + 30 * random.random()),
                            })
            
            current_bar += bars_per_chord
        
        result = {
            "track_name": track_name,
            "chord_progression": chord_progression,
            "duration": duration,
            "note_count": len(notes),
            "notes": notes,
            "status": "configured",
        }
        
        self._processed_samples.append(result)
        logger.info("Ambient pad creado: %d notas, progresión %s", len(notes), chord_progression)
        
        return result
    
    def get_all_processed(self) -> List[Dict[str, Any]]:
        """Retorna todos los samples procesados."""
        return self._processed_samples.copy()


# =============================================================================
# FUNCIONES DE UTILIDAD
# =============================================================================

def arrangement_to_dict(arrangement: ArrangementConfig) -> Dict[str, Any]:
    """
    Serializa un ArrangementConfig a diccionario.
    
    Args:
        arrangement: Configuración a serializar
        
    Returns:
        Diccionario con la estructura completa
    """
    return arrangement.to_dict()


def dict_to_arrangement(data: Dict[str, Any]) -> ArrangementConfig:
    """
    Deserializa un diccionario a ArrangementConfig.
    
    Args:
        data: Diccionario con la configuración
        
    Returns:
        ArrangementConfig reconstruido
    """
    sections = []
    for sec_data in data.get("sections", []):
        clips = []
        for clip_data in sec_data.get("clips", []):
            clips.append(ArrangementClip(
                name=clip_data.get("name", ""),
                track_index=clip_data.get("track_index", 0),
                start_time=clip_data.get("start_time", 0.0),
                duration=clip_data.get("duration", 4.0),
                is_audio=clip_data.get("is_audio", False),
                sample_path=clip_data.get("sample_path", ""),
                notes=clip_data.get("notes", []),
            ))
        
        automations = []
        for auto_data in sec_data.get("automations", []):
            points = [
                AutomationPoint(time=p["time"], value=p["value"])
                for p in auto_data.get("points", [])
            ]
            automations.append(AutomationEnvelope(
                parameter_name=auto_data.get("parameter_name", ""),
                device_name=auto_data.get("device_name", ""),
                points=points,
            ))
        
        sections.append(ArrangementSection(
            name=sec_data.get("name", ""),
            start_bar=sec_data.get("start_bar", 0),
            bars=sec_data.get("bars", 8),
            clips=clips,
            automations=automations,
            energy_level=sec_data.get("energy_level", 0.5),
        ))
    
    markers = [
        SectionMarker(
            name=m.get("name", ""),
            start_bar=m.get("start_bar", 0),
            end_bar=m.get("end_bar", 8),
            color=m.get("color", 0),
        )
        for m in data.get("markers", [])
    ]
    
    return ArrangementConfig(
        total_bars=data.get("total_bars", 64),
        sections=sections,
        markers=markers,
        tempo=data.get("tempo", 95.0),
    )


def get_arrangement_length(arrangement: ArrangementConfig) -> int:
    """
    Retorna la duración total del arrangement en compases.
    
    Args:
        arrangement: Configuración del arrangement
        
    Returns:
        Duración total en compases
    """
    if arrangement.sections:
        last_section = arrangement.sections[-1]
        return last_section.start_bar + last_section.bars
    return arrangement.total_bars


# =============================================================================
# FUNCIONES DE CONVENIENCIA
# =============================================================================

def create_full_arrangement(
    song_config: Dict[str, Any],
    include_fx: bool = True,
    include_automation: bool = True
) -> Dict[str, Any]:
    """
    Crea un arrangement completo con todas las características.
    
    Args:
        song_config: Configuración de la canción
        include_fx: Si incluir efectos FX
        include_automation: Si incluir automatizaciones
        
    Returns:
        Configuración completa del arrangement
    """
    # 1. Crear estructura base
    builder = ArrangementBuilder()
    arrangement = builder.fill_arrangement_with_song(song_config)
    
    # 2. Añadir FX si se solicita
    fx_clips = []
    if include_fx:
        fx_creator = FXCreator()
        
        # Buscar secciones build y crear risers
        for section in arrangement.sections:
            if "build" in section.name.lower():
                fx_clips.extend(
                    fx_creator.create_fx_automation_section(
                        "pre_drop",
                        section.start_bar,
                        section.bars,
                        [len(arrangement.sections)]  # Track de FX
                    )
                )
            elif "break" in section.name.lower():
                fx_clips.extend(
                    fx_creator.create_fx_automation_section(
                        "post_drop",
                        section.start_bar,
                        min(4, section.bars),
                        [len(arrangement.sections)]
                    )
                )
    
    # 3. Añadir automatizaciones si se solicita
    automations = []
    if include_automation:
        auto_engine = AutomationEngine()
        
        # Automatizar filtros en builds
        for section in arrangement.sections:
            if "build" in section.name.lower():
                auto_engine.automate_filter(
                    track_index=5,  # Bass track típico
                    start_bar=section.start_bar,
                    end_bar=section.start_bar + section.bars,
                    start_freq=400,
                    end_freq=8000,
                )
    
    return {
        "arrangement": arrangement.to_dict(),
        "fx_clips": [c.to_dict() for c in fx_clips],
        "automations": [a.to_dict() for a in automations],
    }


# =============================================================================
# CLASE 5: SECTION GENERATOR INTEGRATION (Agente 17)
# =============================================================================

class SectionComposer:
    """
    Compositor de secciones usando SectionGenerator.
    
    Integra el SectionGenerator con el ArrangementEngine para crear
    secciones completas directamente en el arrangement.
    """
    
    def __init__(self, key: str = "Am", bpm: float = 95.0):
        self.key = key
        self.bpm = bpm
        try:
            from section_generator import SectionGenerator, SectionConfig
            self._generator = SectionGenerator(key=key, bpm=bpm)
            self._available = True
        except ImportError:
            self._available = False
            logger.warning("SectionGenerator not available - section features disabled")
    
    def create_section_at_bar(
        self,
        section_type: str,
        at_bar: int,
        duration_bars: int = 8,
        track_offset: int = 0,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Crea una sección musical en una posición específica del arrangement.
        
        Args:
            section_type: Tipo de sección (intro, build, breakdown, chorus, outro, verse)
            at_bar: Posición en compases donde crear la sección
            duration_bars: Duración de la sección en compases
            track_offset: Offset de índices de tracks
            **kwargs: Parámetros específicos del tipo de sección
            
        Returns:
            Configuración de la sección creada
        """
        if not self._available:
            return {
                "status": "error",
                "message": "SectionGenerator not available"
            }
        
        # Generar configuración según tipo
        config = None
        
        if section_type == "intro":
            config = self._generator.generate_intro(
                build_method=kwargs.get("build_method", "gradual"),
                duration=duration_bars,
                start_with_drums=kwargs.get("start_with_drums", False),
                include_fx_riser=kwargs.get("include_fx_riser", True)
            )
        elif section_type == "build":
            config = self._generator.generate_build(
                riser_type=kwargs.get("riser_type", "standard"),
                drum_fill_intensity=kwargs.get("drum_fill_intensity", 0.8),
                duration=duration_bars,
                filter_sweep=kwargs.get("filter_sweep", True)
            )
        elif section_type == "breakdown":
            config = self._generator.generate_breakdown(
                melodic_focus=kwargs.get("melodic_focus", True),
                drum_reduction=kwargs.get("drum_reduction", 0.7),
                duration=duration_bars,
                include_buildup=kwargs.get("include_buildup", True)
            )
        elif section_type == "chorus":
            config = self._generator.generate_chorus(
                max_energy=kwargs.get("max_energy", True),
                all_elements=kwargs.get("all_elements", True),
                duration=duration_bars,
                variation_type=kwargs.get("variation_type", "standard")
            )
        elif section_type == "outro":
            config = self._generator.generate_outro(
                recap_type=kwargs.get("recap_type", "melody_only"),
                ending_style=kwargs.get("ending_style", "fade"),
                duration=duration_bars,
                include_melody=kwargs.get("include_melody", True)
            )
        elif section_type == "verse":
            config = self._generator.generate_verse(
                variation=kwargs.get("variation", "standard"),
                duration=duration_bars,
                include_melody=kwargs.get("include_melody", False)
            )
        else:
            return {
                "status": "error",
                "message": f"Unknown section type: {section_type}"
            }
        
        # Ajustar posiciones de tracks
        adjusted_tracks = []
        for track in config.tracks:
            adjusted_track = track.copy()
            adjusted_track["start_bar"] = at_bar + track.get("start_bar", 0)
            adjusted_tracks.append(adjusted_track)
        
        # Ajustar posiciones de FX
        adjusted_fx = []
        for fx in config.fx:
            adjusted_fx_item = fx.copy()
            adjusted_fx_item["start_bar"] = at_bar + fx.get("start_bar", 0)
            adjusted_fx.append(adjusted_fx_item)
        
        # Ajustar posiciones de automatizaciones
        adjusted_automations = []
        for auto in config.automations:
            adjusted_auto = auto.copy()
            adjusted_auto["start_bar"] = at_bar + auto.get("start_bar", 0)
            adjusted_auto["end_bar"] = at_bar + auto.get("end_bar", duration_bars)
            adjusted_automations.append(adjusted_auto)
        
        result = {
            "status": "success",
            "section_type": section_type,
            "start_bar": at_bar,
            "duration_bars": duration_bars,
            "energy_level": config.energy_level,
            "key": config.key,
            "tracks": adjusted_tracks,
            "fx": adjusted_fx,
            "automations": adjusted_automations,
        }
        
        logger.info("Section created: %s at bar %d, %d bars", section_type, at_bar, duration_bars)
        
        return result
    
    def create_full_song_from_sections(
        self,
        structure_type: str = "standard",
        start_bar: int = 0
    ) -> List[Dict[str, Any]]:
        """
        Crea una canción completa usando SectionGenerator.
        
        Args:
            structure_type: "standard", "extended", "minimal"
            start_bar: Barra inicial
            
        Returns:
            Lista de configuraciones de secciones
        """
        if not self._available:
            return []
        
        sections = self._generator.create_full_song_structure(
            structure_type=structure_type
        )
        
        results = []
        current_bar = start_bar
        
        for section in sections:
            result = self.create_section_at_bar(
                section_type=section.section_type,
                at_bar=current_bar,
                duration_bars=section.duration_bars
            )
            results.append(result)
            current_bar += section.duration_bars
        
        logger.info("Full song structure created: %d sections, %d bars", len(results), current_bar - start_bar)
        
        return results
    
    def is_available(self) -> bool:
        """Verifica si SectionGenerator está disponible."""
        return self._available


# =============================================================================
# EXPORTS
# =============================================================================

__all__ = [
    "ArrangementBuilder",
    "AutomationEngine",
    "FXCreator",
    "SampleProcessor",
    "SectionComposer",
    "ArrangementConfig",
    "ArrangementSection",
    "ArrangementClip",
    "AutomationEnvelope",
    "SectionMarker",
    "arrangement_to_dict",
    "dict_to_arrangement",
    "get_arrangement_length",
    "create_full_arrangement",
]


# =============================================================================
# MAIN / TEST
# =============================================================================

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    print("=" * 70)
    print("ARRANGEMENT ENGINE - Arrangement View and Automation Engine")
    print("=" * 70)
    
    # Test 1: ArrangementBuilder
    print("\n1. Testing ArrangementBuilder...")
    builder = ArrangementBuilder()
    
    song_config = {
        "bpm": 95,
        "structure": "intro_build_drop_break_outro",
        "tracks": [
            {
                "name": "Kick",
                "clips": [
                    {"name": "Kick Pattern", "start_time": 0, "duration": 64, "notes": []}
                ]
            }
        ]
    }
    
    arrangement = builder.fill_arrangement_with_song(song_config)
    print(f"   Total bars: {arrangement.total_bars}")
    print(f"   Sections: {[s.name for s in arrangement.sections]}")
    print(f"   Markers: {[m.name for m in arrangement.markers]}")
    
    # Test 2: AutomationEngine
    print("\n2. Testing AutomationEngine...")
    auto = AutomationEngine()
    
    env = auto.automate_filter(
        track_index=0,
        start_bar=8,
        end_bar=16,
        start_freq=200,
        end_freq=20000,
        curve="exponential"
    )
    print(f"   Filter sweep: {len(env.points)} points")
    
    env2 = auto.automate_volume(
        track_index=0,
        start_bar=0,
        end_bar=8,
        start_vol=0.0,
        end_vol=0.85,
        fade_type="in"
    )
    print(f"   Volume fade: {len(env2.points)} points")
    
    # Test 3: FXCreator
    print("\n3. Testing FXCreator...")
    fx = FXCreator()
    
    riser = fx.create_riser(track_index=7, start_bar=8, duration=8, intensity=0.9)
    print(f"   Riser: {len(riser.notes)} notes")
    
    impact = fx.create_impact(track_index=7, position=16, intensity=1.0)
    print(f"   Impact: note pitch {impact.notes[0]['pitch']}")
    
    fx_section = fx.create_fx_automation_section(
        section_type="pre_drop",
        start_bar=24,
        duration=8,
        track_indices=[7, 8]
    )
    print(f"   FX Section: {len(fx_section)} clips")
    
    # Test 4: SampleProcessor
    print("\n4. Testing SampleProcessor...")
    processor = SampleProcessor()
    
    ambient = processor.create_ambient_layer(
        chord_progression=["Am", "F", "C", "G"],
        duration=32,
        base_octave=4
    )
    print(f"   Ambient pad: {ambient['note_count']} notes")
    
    granular = processor.apply_granular_effect(
        track_index=5,
        grain_size=0.1,
        density=0.6,
        spread=0.4,
        duration_bars=4
    )
    print(f"   Granular effect: {granular['note_count']} grains")
    
    slice_result = processor.slice_and_rearrange(
        sample_path="C:/samples/test.wav",
        num_slices=8,
        new_pattern=[3, 1, 7, 0, 2, 5, 4, 6]
    )
    print(f"   Slices: {slice_result['num_slices']}, pattern: {slice_result['new_pattern']}")
    
    # Test 5: Utilities
    print("\n5. Testing utilities...")
    data = arrangement_to_dict(arrangement)
    print(f"   Serialized: {len(data.keys())} keys")
    
    restored = dict_to_arrangement(data)
    print(f"   Restored: {len(restored.sections)} sections")
    
    length = get_arrangement_length(arrangement)
    print(f"   Total length: {length} bars")
    
    # Test 6: Full pipeline
    print("\n6. Testing full arrangement pipeline...")
    full = create_full_arrangement(song_config, include_fx=True, include_automation=True)
    print(f"   Full arrangement keys: {list(full.keys())}")
    print(f"   FX clips: {len(full['fx_clips'])}")
    
    print("\n" + "=" * 70)
    print("All tests completed successfully!")
    print("=" * 70)
