"""
Harmony Engine - Motor de Inteligencia Musical Avanzada para AbletonMCP_AI.

Este módulo proporciona análisis musical sofisticado, generación de armonías,
variación inteligente de loops, manipulación avanzada de samples, y
comparación con referencias profesionales.

Clases principales:
- ProjectAnalyzer: Análisis de key, energía y balance de secciones
- CounterMelodyGenerator: Generación de contra-melodías y armonías
- VariationEngine: Variación inteligente de loops y secciones
- SampleIntelligence: Manipulación avanzada de samples
- ReferenceMatcher: Comparación y adaptación a referencias

Tareas implementadas:
- Parte 1 (T041-T045): Análisis y Adaptación
- Parte 2 (T046-T050): Variación Inteligente
- Parte 3 (T051-T055): Samples Inteligentes
- Parte 4 (T056-T060): Referencia y Comparación
"""
import json
import logging
import os
import random
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union

import numpy as np

logger = logging.getLogger("HarmonyEngine")


# =============================================================================
# DATACLASSES - Perfiles y Métricas Musicales
# =============================================================================

@dataclass
class EnergyCurve:
    """Perfil de energía a lo largo de una canción o sección.
    
    Atributos:
        bars: Posiciones en compases donde se midió la energía
        levels: Niveles de energía (0.0-1.0) en cada posición
        section_names: Nombres de las secciones correspondientes
    """
    bars: List[int] = field(default_factory=list)
    levels: List[float] = field(default_factory=list)
    section_names: List[str] = field(default_factory=list)
    
    def get_level_at(self, bar: int) -> float:
        """Obtiene nivel de energía en un compás específico."""
        if not self.bars:
            return 0.5
        closest_idx = min(range(len(self.bars)), key=lambda i: abs(self.bars[i] - bar))
        return self.levels[closest_idx] if closest_idx < len(self.levels) else 0.5
    
    def get_average(self, start_bar: int, end_bar: int) -> float:
        """Calcula energía promedio entre dos compases."""
        relevant = [l for b, l in zip(self.bars, self.levels) if start_bar <= b <= end_bar]
        return np.mean(relevant) if relevant else 0.5
    
    def get_peak_level(self) -> float:
        """Retorna el nivel de energía máximo."""
        return max(self.levels) if self.levels else 0.0
    
    def get_trough_level(self) -> float:
        """Retorna el nivel de energía mínimo."""
        return min(self.levels) if self.levels else 0.0
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "bars": self.bars,
            "levels": self.levels,
            "section_names": self.section_names,
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "EnergyCurve":
        return cls(
            bars=data.get("bars", []),
            levels=data.get("levels", []),
            section_names=data.get("section_names", []),
        )


@dataclass
class SpectrumProfile:
    """Perfil espectral con frecuencias y magnitudes por banda.
    
    Atributos:
        frequencies: Lista de frecuencias en Hz
        magnitudes: Lista de magnitudes en dB
        low_energy: Energía en frecuencias bajas (20-250 Hz)
        low_mid_energy: Energía en low-mid (250-500 Hz)
        mid_energy: Energía en frecuencias medias (500-2000 Hz)
        high_mid_energy: Energía en high-mid (2000-4000 Hz)
        high_energy: Energía en frecuencias altas (4000-20000 Hz)
    """
    frequencies: List[float] = field(default_factory=list)
    magnitudes: List[float] = field(default_factory=list)
    low_energy: float = 0.0
    low_mid_energy: float = 0.0
    mid_energy: float = 0.0
    high_mid_energy: float = 0.0
    high_energy: float = 0.0
    
    def get_balance_score(self) -> float:
        """Retorna score de balance espectral (0.0-1.0)."""
        energies = [self.low_energy, self.low_mid_energy, self.mid_energy, 
                   self.high_mid_energy, self.high_energy]
        if not any(energies):
            return 0.5
        ideal = [0.25, 0.15, 0.25, 0.20, 0.15]
        normalized = [e/sum(energies) for e in energies]
        deviation = sum(abs(n - i) for n, i in zip(normalized, ideal))
        return max(0.0, 1.0 - deviation)
    
    def get_dominant_frequency_range(self) -> str:
        """Determina el rango de frecuencia dominante."""
        energies = {
            "low": self.low_energy,
            "low_mid": self.low_mid_energy,
            "mid": self.mid_energy,
            "high_mid": self.high_mid_energy,
            "high": self.high_energy,
        }
        return max(energies.items(), key=lambda x: x[1])[0]
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "frequencies": self.frequencies,
            "magnitudes": self.magnitudes,
            "low_energy": self.low_energy,
            "low_mid_energy": self.low_mid_energy,
            "mid_energy": self.mid_energy,
            "high_mid_energy": self.high_mid_energy,
            "high_energy": self.high_energy,
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "SpectrumProfile":
        return cls(
            frequencies=data.get("frequencies", []),
            magnitudes=data.get("magnitudes", []),
            low_energy=data.get("low_energy", 0.0),
            low_mid_energy=data.get("low_mid_energy", 0.0),
            mid_energy=data.get("mid_energy", 0.0),
            high_mid_energy=data.get("high_mid_energy", 0.0),
            high_energy=data.get("high_energy", 0.0),
        )


@dataclass
class StereoWidth:
    """Ancho estéreo por bandas de frecuencia.
    
    Atributos:
        low: Ancho en frecuencias bajas 20-250 Hz (ideal: mono)
        mid_low: Ancho en rango 250-500 Hz
        mid: Ancho en rango 500-2000 Hz
        high: Ancho en frecuencias altas 2000+ Hz (ideal: ancho)
        overall_width: Ancho estéreo general promedio
    """
    low: float = 0.0
    mid_low: float = 0.0
    mid: float = 0.0
    high: float = 0.0
    overall_width: float = 0.0
    
    def is_balanced(self) -> bool:
        """Verifica si el ancho estéreo está balanceado."""
        return self.low <= 0.3 and self.high >= 0.5
    
    def get_recommendations(self) -> List[str]:
        """Genera recomendaciones de ajuste de stereo width."""
        recs = []
        if self.low > 0.3:
            recs.append("Reduce stereo width en frecuencias bajas (<250Hz) para evitar conflictos de fase")
        if self.high < 0.5:
            recs.append("Aumenta stereo width en frecuencias altas (>2kHz) para más ambiente")
        if self.mid < 0.3:
            recs.append("Considera aumentar ancho estéreo en rango medio para elementos principales")
        return recs
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "low": self.low,
            "mid_low": self.mid_low,
            "mid": self.mid,
            "high": self.high,
            "overall_width": self.overall_width,
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "StereoWidth":
        return cls(
            low=data.get("low", 0.0),
            mid_low=data.get("mid_low", 0.0),
            mid=data.get("mid", 0.0),
            high=data.get("high", 0.0),
            overall_width=data.get("overall_width", 0.0),
        )


@dataclass
class SimilarityScore:
    """Puntuación de similitud multidimensional entre proyectos.
    
    Atributos:
        bpm_score: Similitud de BPM (0.0-1.0)
        key_score: Similitud de tonalidad (0.0-1.0)
        energy_score: Similitud de curva de energía (0.0-1.0)
        spectrum_score: Similitud de espectro (0.0-1.0)
        width_score: Similitud de ancho estéreo (0.0-1.0)
        ...weights: Pesos para cálculo del score total
    """
    bpm_score: float = 0.0
    key_score: float = 0.0
    energy_score: float = 0.0
    spectrum_score: float = 0.0
    width_score: float = 0.0
    bpm_weight: float = 0.20
    key_weight: float = 0.15
    energy_weight: float = 0.25
    spectrum_weight: float = 0.25
    width_weight: float = 0.15
    
    @property
    def total(self) -> float:
        """Calcula score total ponderado."""
        total_weight = sum([self.bpm_weight, self.key_weight, self.energy_weight, 
                           self.spectrum_weight, self.width_weight])
        if total_weight == 0:
            return 0.0
        score = (
            self.bpm_score * self.bpm_weight +
            self.key_score * self.key_weight +
            self.energy_score * self.energy_weight +
            self.spectrum_score * self.spectrum_weight +
            self.width_score * self.width_weight
        ) / total_weight
        return round(score, 3)
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "bpm_score": self.bpm_score,
            "key_score": self.key_score,
            "energy_score": self.energy_score,
            "spectrum_score": self.spectrum_score,
            "width_score": self.width_score,
            "total": self.total,
            "weights": {
                "bpm": self.bpm_weight,
                "key": self.key_weight,
                "energy": self.energy_weight,
                "spectrum": self.spectrum_weight,
                "width": self.width_weight,
            }
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "SimilarityScore":
        weights = data.get("weights", {})
        return cls(
            bpm_score=data.get("bpm_score", 0.0),
            key_score=data.get("key_score", 0.0),
            energy_score=data.get("energy_score", 0.0),
            spectrum_score=data.get("spectrum_score", 0.0),
            width_score=data.get("width_score", 0.0),
            bpm_weight=weights.get("bpm", 0.20),
            key_weight=weights.get("key", 0.15),
            energy_weight=weights.get("energy", 0.25),
            spectrum_weight=weights.get("spectrum", 0.25),
            width_weight=weights.get("width", 0.15),
        )


# =============================================================================
# PARTE 1 - Análisis y Adaptación (T041-T045)
# =============================================================================

class ProjectAnalyzer:
    """
    Analiza proyectos musicales para extraer información clave.
    
    Métodos:
    - T041: analyze_project_key() - Detecta key predominante de notas MIDI
    - T042: harmonize_track() - Genera notas armonizadas con progresión
    - T043: detect_energy_curve() - Grafica energía de la canción
    - T044: balance_sections() - Ajusta energía entre secciones
    """
    
    NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
    KEY_PROFILES = {
        'C': [1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0],
        'G': [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1],
        'D': [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
        'A': [0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0],
        'E': [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],
        'Am': [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
        'Em': [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        'Dm': [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
        'Gm': [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
        'Cm': [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    }
    
    def analyze_project_key(self, tracks: List[Dict[str, Any]]) -> Dict[str, Any]:
        """
        T041: Analiza notas MIDI de múltiples tracks y detecta la key predominante.
        
        Args:
            tracks: Lista de tracks con información de notas MIDI
            
        Returns:
            Dict con key detectada, confianza, keys alternativas, distribución de notas
        """
        all_notes = []
        for track in tracks:
            if 'notes' in track:
                all_notes.extend(track['notes'])
            elif 'clips' in track:
                for clip in track['clips']:
                    if 'notes' in clip:
                        all_notes.extend(clip['notes'])
        
        if not all_notes:
            return {"key": "Am", "confidence": 0.0, "alternative_keys": [], 
                   "note_distribution": {}, "scale_type": "minor"}
        
        pitches = [n['pitch'] % 12 for n in all_notes if 'pitch' in n]
        if not pitches:
            return {"key": "Am", "confidence": 0.0, "alternative_keys": [],
                   "note_distribution": {}, "scale_type": "minor"}
        
        chroma_counts = Counter(pitches)
        total = len(pitches)
        distribution = [chroma_counts.get(i, 0) / total for i in range(12)]
        
        best_key, best_score = None, -1
        scores = {}
        for key_name, profile in self.KEY_PROFILES.items():
            correlation = np.corrcoef(distribution, profile)[0, 1]
            if np.isnan(correlation):
                correlation = 0.0
            scores[key_name] = correlation
            if correlation > best_score:
                best_score, best_key = correlation, key_name
        
        alt_keys = sorted(scores.items(), key=lambda x: x[1], reverse=True)[1:4]
        scale_type = "major" if len(best_key) == 1 or best_key[-1] != 'm' else "minor"
        
        return {
            "key": best_key,
            "confidence": round(best_score, 3),
            "alternative_keys": [{"key": k, "confidence": round(s, 3)} for k, s in alt_keys],
            "note_distribution": {self.NOTE_NAMES[i]: round(chroma_counts.get(i, 0) / total, 3) for i in range(12)},
            "scale_type": scale_type,
            "total_notes_analyzed": total,
        }
    
    def harmonize_track(self, track_index: int, chord_progression: List[str],
                       harmony_level: str = "triads") -> Dict[str, Any]:
        """
        T042: Genera notas armonizadas para un track basado en progresión de acordes.
        
        Args:
            track_index: Índice del track a armonizar
            chord_progression: Lista de acordes (e.g., ['Am', 'F', 'C', 'G'])
            harmony_level: Nivel de armonía ('triads', 'sevenths', 'extended')
            
        Returns:
            Dict con notas generadas y configuración
        """
        chord_structures = {
            'Am': [0, 3, 7], 'Dm': [2, 5, 9], 'Em': [4, 7, 11],
            'Gm': [7, 10, 2], 'Bm': [11, 2, 6],
            'C': [0, 4, 7], 'F': [5, 9, 0], 'G': [7, 11, 2],
            'D': [2, 6, 9], 'A': [9, 1, 4], 'E': [4, 8, 11],
        }
        seventh_extensions = {
            'Am': 10, 'Dm': 0, 'Em': 2, 'Gm': 5, 'Bm': 9,
            'C': 11, 'F': 4, 'G': 6, 'D': 1, 'A': 8, 'E': 3,
        }
        
        generated_notes = []
        for bar_idx, chord in enumerate(chord_progression):
            if chord not in chord_structures:
                continue
            base_notes = chord_structures[chord][:]
            if harmony_level in ('sevenths', 'extended') and chord in seventh_extensions:
                base_notes.append(seventh_extensions[chord])
            
            for note_offset in base_notes:
                pitch = (69 + note_offset) % 12 + 57
                generated_notes.append({
                    "pitch": pitch,
                    "start_time": bar_idx * 4.0,
                    "duration": 4.0,
                    "velocity": 80,
                })
        
        return {
            "track_index": track_index,
            "chord_progression": chord_progression,
            "harmony_level": harmony_level,
            "notes_generated": len(generated_notes),
            "notes": generated_notes,
            "bars_covered": len(chord_progression),
        }
    
    def detect_energy_curve(self, arrangement: Dict[str, Any]) -> EnergyCurve:
        """
        T043: Detecta y grafica la curva de energía del arreglo.
        
        Args:
            arrangement: Dict con información de secciones y tracks
            
        Returns:
            EnergyCurve con niveles por compás
        """
        sections = arrangement.get('sections', [])
        tracks = arrangement.get('tracks', [])
        
        if not sections:
            return EnergyCurve(
                bars=list(range(0, 64, 4)),
                levels=[0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.8, 0.6, 0.9, 1.0, 0.7, 0.5, 0.4, 0.3],
                section_names=['Intro', 'Build 1', 'Build 2', 'Drop A', 'Break', 'Build 3', 'Drop B', 'Outro']
            )
        
        section_energy = {
            'intro': 0.30, 'verse': 0.40, 'build': 0.60, 'buildup': 0.60,
            'pre-chorus': 0.60, 'drop': 1.00, 'chorus': 0.90, 'hook': 0.90,
            'break': 0.40, 'breakdown': 0.40, 'bridge': 0.50, 'outro': 0.30,
        }
        
        bars, levels, names, current_bar = [], [], [], 0
        for section in sections:
            name = section.get('name', 'Unknown').lower()
            duration = section.get('duration_bars', 8)
            base_energy = next((v for k, v in section_energy.items() if k in name), 0.5)
            density = section.get('active_tracks', len(tracks)) / max(len(tracks), 1)
            adjusted = base_energy * (0.7 + 0.3 * density)
            
            bars.append(current_bar)
            levels.append(round(min(1.0, adjusted), 2))
            names.append(name.title())
            current_bar += duration
        
        return EnergyCurve(bars=bars, levels=levels, section_names=names)
    
    def balance_sections(self, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """
        T044: Ajusta los niveles de energía entre secciones.
        
        Args:
            sections: Lista de secciones a balancear
            
        Returns:
            Lista de secciones con niveles ajustados
        """
        targets = {
            'intro': 0.30, 'verse': 0.40, 'build': 0.60, 'buildup': 0.60,
            'pre-chorus': 0.60, 'drop': 1.00, 'chorus': 0.90, 'hook': 0.90,
            'break': 0.40, 'breakdown': 0.40, 'bridge': 0.50, 'outro': 0.30,
        }
        
        balanced = []
        for section in sections:
            name = section.get('name', 'Unknown').lower()
            current = section.get('energy_level', 0.5)
            target = next((v for k, v in targets.items() if k in name), 0.5)
            adjustment = target - current
            
            suggestions = []
            if adjustment > 0.2:
                suggestions.extend([
                    f"Añadir {int(adjustment * 100)}% más elementos",
                    "Subir volumen de drums"
                ])
            elif adjustment < -0.2:
                suggestions.extend([
                    f"Reducir {int(abs(adjustment) * 100)}% densidad",
                    "Bajar volumen de pads"
                ])
            
            balanced.append({
                **section,
                "target_energy": target,
                "current_energy": current,
                "adjustment_needed": round(adjustment, 2),
                "suggested_adjustments": suggestions,
                "is_balanced": abs(adjustment) < 0.15,
            })
        
        return balanced


class CounterMelodyGenerator:
    """
    Genera contra-melodías que complementan melodías principales.
    
    T045: generate_counter_melody() - Usa intervalos consonantes: 3rds, 6ths
    """
    
    INTERVALS = {
        'third_major': 4, 'third_minor': 3, 'fifth': 7,
        'sixth_major': 9, 'sixth_minor': 8, 'octave': 12, 'fourth': 5,
    }
    MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11]
    MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10]
    
    def generate_counter_melody(self, main_melody_track: Dict[str, Any],
                               harmony_level: str = "thirds") -> Dict[str, Any]:
        """
        T045: Genera una contra-melodía basada en la melodía principal.
        
        Args:
            main_melody_track: Track con la melodía principal
            harmony_level: Nivel de armonía ('thirds', 'sixths', 'mixed', 'complementary')
            
        Returns:
            Dict con notas de contra-melodía generadas
        """
        notes = main_melody_track.get('notes', [])
        if not notes:
            return {"notes": [], "harmony_level": harmony_level, "status": "empty_source"}
        
        scale = self._detect_scale(notes)
        key_center = self._detect_key_center(notes)
        counter_notes = []
        
        for note in notes:
            pitch = note.get('pitch', 60)
            interval = self._select_interval(pitch, scale, harmony_level, key_center)
            counter_pitch = self._quantize_to_scale(pitch + interval, scale, key_center)
            
            if harmony_level in ('thirds', 'fifths') and counter_pitch > pitch + 4:
                counter_pitch -= 12
            elif harmony_level == 'sixths' and counter_pitch < pitch:
                counter_pitch += 12
            
            counter_notes.append({
                "pitch": counter_pitch,
                "start_time": note.get('start_time', 0),
                "duration": note.get('duration', 0.25),
                "velocity": int(note.get('velocity', 100) * 0.85),
            })
        
        return {
            "notes": counter_notes,
            "harmony_level": harmony_level,
            "source_note_count": len(notes),
            "generated_note_count": len(counter_notes),
            "detected_scale": scale,
            "key_center": key_center,
            "status": "success",
        }
    
    def _detect_scale(self, notes: List[Dict[str, Any]]) -> List[int]:
        pitches = [n['pitch'] % 12 for n in notes if 'pitch' in n]
        if not pitches:
            return self.MINOR_SCALE
        counts = Counter(pitches)
        major_score = sum(counts.get(p, 0) for p in self.MAJOR_SCALE)
        minor_score = sum(counts.get(p, 0) for p in self.MINOR_SCALE)
        return self.MAJOR_SCALE if major_score > minor_score else self.MINOR_SCALE
    
    def _detect_key_center(self, notes: List[Dict[str, Any]]) -> int:
        pitches = [n['pitch'] % 12 for n in notes if 'pitch' in n]
        return Counter(pitches).most_common(1)[0][0] if pitches else 0
    
    def _select_interval(self, pitch: int, scale: List[int], level: str, key_center: int) -> int:
        relative = (pitch % 12 - key_center) % 12
        if level == "thirds":
            interval = self.INTERVALS['third_minor'] if 3 in scale else self.INTERVALS['third_major']
            return interval * (-1 if relative in scale[:4] else 1)
        elif level == "sixths":
            return self.INTERVALS['sixth_minor'] if 3 in scale else self.INTERVALS['sixth_major']
        elif level == "fifths":
            return -self.INTERVALS['fifth']
        elif level == "mixed":
            return random.choice([
                self.INTERVALS['third_minor'] if 3 in scale else self.INTERVALS['third_major'],
                self.INTERVALS['sixth_minor'] if 3 in scale else self.INTERVALS['sixth_major'],
                self.INTERVALS['fifth'],
            ])
        return 3
    
    def _quantize_to_scale(self, pitch: int, scale: List[int], key_center: int) -> int:
        relative = (pitch % 12 - key_center) % 12
        if relative in scale:
            return pitch
        distances = [(s, abs(relative - s)) for s in scale]
        distances.extend([(s + 12, abs(relative - (s + 12))) for s in scale])
        distances.extend([(s - 12, abs(relative - (s - 12))) for s in scale])
        closest = min(distances, key=lambda x: x[1])[0]
        return (pitch // 12) * 12 + ((key_center + closest) % 12)


# =============================================================================
# PARTE 2 - Variación Inteligente (T046-T050)
# =============================================================================

class VariationEngine:
    """
    Motor de variación inteligente para loops y secciones.
    
    Métodos:
    - T046: variate_loop() - Genera variación de loop
    - T047: add_call_and_response() - Call: 2 bars, Response: 2 bars
    - T048: generate_breakdown() - Crea breakdown strip down
    - T049: generate_drop_variation() - Drop A vs Drop B
    - T050: create_outro() - Outro basado en intro con fade
    """
    
    def variate_loop(self, loop_clips: List[Dict[str, Any]],
                    variation_intensity: float = 0.5) -> List[Dict[str, Any]]:
        """
        T046: Genera una variación de loop existente.
        
        Args:
            loop_clips: Lista de clips a variar
            variation_intensity: 0.0-1.0 (qué tan drástica la variación)
            
        Returns:
            Lista de clips variados
        """
        varied_clips = []
        techniques = []
        if variation_intensity > 0.2:
            techniques.append('velocity')
        if variation_intensity > 0.4:
            techniques.append('timing')
        if variation_intensity > 0.6:
            techniques.append('octave')
        if variation_intensity > 0.7:
            techniques.append('ornament')
        if variation_intensity > 0.8:
            techniques.append('rests')
        
        for clip in loop_clips:
            notes = clip.get('notes', [])
            if not notes:
                varied_clips.append(clip)
                continue
            
            varied_notes = notes[:]
            for technique in techniques:
                varied_notes = self._apply_technique(varied_notes, technique, variation_intensity)
            
            varied_clips.append({
                **clip,
                "notes": varied_notes,
                "is_variation": True,
                "original_clip": clip.get('name', 'unknown'),
                "variation_intensity": variation_intensity,
                "techniques_applied": techniques,
            })
        
        return varied_clips
    
    def _apply_technique(self, notes: List[Dict[str, Any]],
                        technique: str, intensity: float) -> List[Dict[str, Any]]:
        varied = []
        
        if technique == 'velocity':
            for note in notes:
                vel = note.get('velocity', 100)
                variation = random.uniform(-20, 20) * intensity
                varied.append({**note, "velocity": max(1, min(127, int(vel + variation)))})
        
        elif technique == 'timing':
            for note in notes:
                start = note.get('start_time', 0)
                varied.append({**note, "start_time": max(0, start + random.uniform(-0.05, 0.05) * intensity)})
        
        elif technique == 'octave':
            for note in notes:
                if random.random() < intensity * 0.3:
                    pitch = note.get('pitch', 60)
                    varied.append({**note, "pitch": pitch + (12 if random.random() > 0.5 else -12)})
                else:
                    varied.append(note)
        
        elif technique == 'ornament':
            for note in notes:
                varied.append(note)
                if random.random() < intensity * 0.2:
                    varied.append({
                        "pitch": note.get('pitch', 60) + random.choice([-1, 1, 2]),
                        "start_time": note.get('start_time', 0) - 0.02,
                        "duration": 0.02,
                        "velocity": min(127, int(note.get('velocity', 100) * 0.8)),
                    })
        
        elif technique == 'rests':
            for note in notes:
                if random.random() > intensity * 0.15:
                    varied.append(note)
        
        return varied if varied else notes
    
    def add_call_and_response(self, phrase_track: Dict[str, Any],
                             response_length: int = 2) -> Dict[str, Any]:
        """
        T047: Añade patrón Call and Response.
        Call: 2 bars, Response: 2 bars
        
        Args:
            phrase_track: Track con la frase principal
            response_length: Longitud del response en compases
            
        Returns:
            Dict con notas de call y response
        """
        notes = phrase_track.get('notes', [])
        if not notes:
            return {"call_notes": [], "response_notes": []}
        
        max_time = max(n.get('start_time', 0) for n in notes)
        mid_point = max_time / 2
        call_notes = [n for n in notes if n.get('start_time', 0) < mid_point]
        
        transposition = random.choice([-7, -5, -3, 0, 3, 5, 7])
        response_notes = []
        for note in call_notes:
            response_notes.append({
                "pitch": note.get('pitch', 60) + transposition,
                "start_time": note.get('start_time', 0) + mid_point,
                "duration": note.get('duration', 0.25) * random.uniform(0.8, 1.2),
                "velocity": max(1, min(127, int(note.get('velocity', 100) + random.uniform(-15, 15)))),
            })
        
        return {
            "call_notes": call_notes,
            "response_notes": response_notes,
            "transposition_semitones": transposition,
            "call_bars": 2,
            "response_bars": response_length,
            "pattern": "call_response",
        }
    
    def generate_breakdown(self, full_sections: List[Dict[str, Any]],
                          intensity: float = 0.3) -> Dict[str, Any]:
        """
        T048: Crea un breakdown strip down reduciendo elementos.
        
        Args:
            full_sections: Secciones completas con todos los tracks
            intensity: Cuánto mantener (0.3 = 30% de elementos)
            
        Returns:
            Dict con sección breakdown generada
        """
        if not full_sections:
            return {"tracks": [], "duration_bars": 8, "section_type": "breakdown"}
        
        priority_roles = ['melody', 'lead', 'vocal', 'pad', 'atmosphere']
        breakdown_tracks = []
        
        for section in full_sections:
            tracks = sorted(
                section.get('tracks', []),
                key=lambda t: priority_roles.index(t.get('role', '')) if t.get('role', '') in priority_roles else 999
            )
            kept = tracks[:max(1, int(len(tracks) * intensity))]
            breakdown_tracks.extend([self._reduce_track_intensity(t, 0.5) for t in kept])
        
        return {
            "tracks": breakdown_tracks,
            "duration_bars": 8,
            "section_type": "breakdown",
            "intensity": intensity,
            "tracks_count": len(breakdown_tracks),
            "original_tracks_count": sum(len(s.get('tracks', [])) for s in full_sections),
        }
    
    def _reduce_track_intensity(self, track: Dict[str, Any], factor: float) -> Dict[str, Any]:
        return {
            **track,
            "notes": [{**n, "velocity": int(n.get('velocity', 100) * factor)} for n in track.get('notes', [])],
            "volume_reduction_factor": factor,
        }
    
    def generate_drop_variation(self, drop_section: Dict[str, Any],
                             variation_type: str = "alt") -> Dict[str, Any]:
        """
        T049: Genera variación de drop (Drop A vs Drop B).
        
        Args:
            drop_section: Sección drop original
            variation_type: 'alt' para alternativa, 'intense' para más intenso
            
        Returns:
            Dict con drop variado
        """
        varied_tracks = []
        
        for track in drop_section.get('tracks', []):
            notes = track.get('notes', [])
            role = track.get('role', '')
            
            if variation_type == "alt":
                if role in ['drums', 'percussion']:
                    varied_notes = self._alternate_drum_pattern(notes)
                elif role in ['bass', 'sub']:
                    varied_notes = self._invert_bass_line(notes)
                else:
                    varied_notes = notes
            else:
                varied_notes = self._intensify_drums(notes) if role in ['drums', 'percussion'] else notes
            
            varied_tracks.append({
                **track,
                "notes": varied_notes,
                "is_variation": True,
                "variation_type": variation_type,
            })
        
        return {
            "tracks": varied_tracks,
            "section_type": f"drop_{variation_type}",
            "duration_bars": drop_section.get('duration_bars', 8),
            "variation_of": drop_section.get('name', 'unknown'),
        }
    
    def _alternate_drum_pattern(self, notes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        varied = []
        for note in notes:
            if note.get('pitch', 36) in [38, 40] and random.random() < 0.3:
                varied.append({**note, "start_time": note.get('start_time', 0) + 0.5})
            else:
                varied.append(note)
        return varied
    
    def _invert_bass_line(self, notes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        if not notes:
            return notes
        center = sum(n.get('pitch', 60) for n in notes) / len(notes)
        return [{**note, "pitch": int(2 * center - note.get('pitch', 60))} for note in notes]
    
    def _intensify_drums(self, notes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        varied = notes[:]
        for note in notes:
            if note.get('pitch', 0) in [38, 40]:
                varied.append({
                    **note,
                    "start_time": note.get('start_time', 0) + 0.25,
                    "velocity": 40,
                    "is_ghost": True,
                })
        return varied
    
    def create_outro(self, intro_section: Dict[str, Any],
                    fade_duration: int = 8) -> Dict[str, Any]:
        """
        T050: Crea un outro basado en la intro con fade out.
        
        Args:
            intro_section: Sección intro como base
            fade_duration: Duración del fade en compases
            
        Returns:
            Dict con sección outro generada
        """
        outro_tracks = []
        
        for track in intro_section.get('tracks', []):
            faded_notes = []
            for note in track.get('notes', []):
                fade_factor = max(0.0, 1.0 - (note.get('start_time', 0) / (fade_duration * 4)))
                faded_notes.append({**note, "velocity": int(note.get('velocity', 100) * fade_factor)})
            outro_tracks.append({**track, "notes": faded_notes, "has_fade": True})
        
        return {
            "tracks": outro_tracks,
            "section_type": "outro",
            "duration_bars": fade_duration,
            "based_on": "intro",
            "fade_duration": fade_duration,
        }


# =============================================================================
# PARTE 3 - Samples Inteligentes (T051-T055)
# =============================================================================

class SampleIntelligence:
    """
    Inteligencia avanzada para manipulación de samples.
    
    Métodos:
    - T051: find_and_replace_sample() - Busca alternativa similar
    - T052: layer_samples() - Layer 2+ samples
    - T053: create_sample_chain() - Encadena samples
    - T054: generate_from_sample() - Genera canción basada en sample
    - T055: create_vocal_chops() - Crea chops mapeados a Drum Rack
    """
    
    def __init__(self, library_path: Optional[str] = None):
        self.library_path = library_path or str(
            Path(r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\libreria\reggaeton")
        )
        self._embedding_engine = None
    
    def _get_embedding_engine(self):
        if self._embedding_engine is None:
            try:
                from .embedding_engine import EmbeddingEngine
                self._embedding_engine = EmbeddingEngine()
            except ImportError:
                self._embedding_engine = None
        return self._embedding_engine
    
    def find_and_replace_sample(self, current_sample_path: str,
                               similarity_threshold: float = 0.7) -> Dict[str, Any]:
        """
        T051: Busca una alternativa similar al sample actual.
        
        Args:
            current_sample_path: Ruta al sample actual
            similarity_threshold: Score mínimo de similitud (0.0-1.0)
            
        Returns:
            Dict con alternativas encontradas
        """
        engine = self._get_embedding_engine()
        if engine is None:
            return self._fallback_find_similar(current_sample_path, similarity_threshold)
        
        try:
            similar = engine.find_similar(current_sample_path, top_n=10)
            candidates = [s for s in similar if s.get('similarity', 0) >= similarity_threshold]
            return {
                "original_sample": current_sample_path,
                "alternatives": candidates[:5],
                "threshold_used": similarity_threshold,
                "matches_found": len(candidates),
            }
        except:
            return self._fallback_find_similar(current_sample_path, similarity_threshold)
    
    def _fallback_find_similar(self, sample_path: str, threshold: float) -> Dict[str, Any]:
        sample_dir = Path(sample_path).parent
        sample_name = Path(sample_path).stem.lower()
        alternatives = []
        
        if sample_dir.exists():
            for f in sample_dir.glob("*.wav"):
                if f.name.lower() != sample_path.lower():
                    words1 = set(sample_name.split('_'))
                    words2 = set(f.stem.lower().split('_'))
                    if words1 & words2:
                        sim = len(words1 & words2) / len(words1 | words2)
                        if sim >= threshold:
                            alternatives.append({
                                "path": str(f),
                                "name": f.name,
                                "similarity": round(sim, 2),
                            })
        
        return {
            "original_sample": sample_path,
            "alternatives": alternatives[:5],
            "threshold_used": threshold,
            "matches_found": len(alternatives),
            "method": "fallback_name_matching",
        }
    
    def layer_samples(self, track_index: int, sample_paths: List[str],
                     volumes: Optional[List[float]] = None) -> Dict[str, Any]:
        """
        T052: Crea un layer de 2+ samples.
        
        Args:
            track_index: Track donde colocar los samples
            sample_paths: Lista de rutas de samples
            volumes: Volumen para cada sample (0.0-1.0)
            
        Returns:
            Dict con configuración del layer
        """
        valid = [p for p in sample_paths if os.path.exists(p)]
        if len(valid) < 2:
            return {"error": "Se necesitan al menos 2 samples válidos para layer"}
        
        if volumes is None:
            volumes = [1.0 / len(valid)] * len(valid)
        
        total = sum(volumes)
        if total > 1.0:
            volumes = [v / total for v in volumes]
        
        layers = []
        for i, (path, vol) in enumerate(zip(valid, volumes)):
            layers.append({
                "sample_path": path,
                "sample_name": Path(path).name,
                "volume": round(vol, 3),
                "track_position": i,
                "pan": 0.0 if i == 0 else random.choice([-0.3, 0.3]),
            })
        
        return {
            "track_index": track_index,
            "num_layers": len(layers),
            "layers": layers,
            "total_volume": round(sum(l['volume'] for l in layers), 3),
            "layering_strategy": "equal_blend" if len(set(volumes)) == 1 else "weighted_blend",
        }
    
    def create_sample_chain(self, sample_sequence: List[str],
                           transition_duration: float = 1.0) -> Dict[str, Any]:
        """
        T053: Encadena múltiples samples en secuencia.
        
        Args:
            sample_sequence: Lista ordenada de samples
            transition_duration: Duración de transiciones en compases
            
        Returns:
            Dict con cadena de samples configurada
        """
        valid = [p for p in sample_sequence if os.path.exists(p)]
        if not valid:
            return {"error": "Secuencia vacía"}
        
        chain = []
        current_pos = 0.0
        
        for i, path in enumerate(valid):
            chain.append({
                "sample_path": path,
                "sample_name": Path(path).name,
                "start_bar": current_pos,
                "duration_bars": 4.0,
                "transition_in": transition_duration if i > 0 else 0.0,
                "transition_out": transition_duration if i < len(valid) - 1 else 0.0,
            })
            current_pos += 4.0
        
        return {
            "chain": chain,
            "total_samples": len(chain),
            "total_duration_bars": current_pos,
            "transition_duration": transition_duration,
            "chain_type": "sequential",
        }
    
    def generate_from_sample(self, seed_sample_path: str,
                            style: str = "inspired") -> Dict[str, Any]:
        """
        T054: Genera canción/idea basada en un sample seed.
        
        Args:
            seed_sample_path: Ruta al sample de inspiración
            style: Estilo de generación ('inspired', 'similar', 'remix')
            
        Returns:
            Dict con configuración de canción generada
        """
        if not os.path.exists(seed_sample_path):
            return {"error": f"Sample no encontrado: {seed_sample_path}"}
        
        engine = self._get_embedding_engine()
        features = engine.analyzer.get_features(seed_sample_path) if engine and hasattr(engine, 'analyzer') else {}
        similar = engine.find_similar(seed_sample_path, top_n=10) if engine else []
        
        bpm = features.get('bpm', 95)
        key = features.get('key', 'Am')
        
        structures = {
            "inspired": ["intro", "build", "drop", "break", "drop", "outro"],
            "similar": ["intro", "verse", "build", "drop", "break", "drop", "outro"],
            "remix": ["intro_seed", "build", "drop_seed_mix", "break", "drop_remix", "outro_seed"],
        }
        
        return {
            "seed_sample": seed_sample_path,
            "style": style,
            "extracted_features": features,
            "suggested_bpm": bpm,
            "suggested_key": key,
            "structure": structures.get(style, structures["inspired"]),
            "similar_samples_for_arrangement": similar[:5],
            "recommended_tracks": self._suggest_tracks_for_style(style),
        }
    
    def _suggest_tracks_for_style(self, style: str) -> List[Dict[str, Any]]:
        base = [
            {"role": "kick", "type": "drum", "priority": "high"},
            {"role": "snare", "type": "drum", "priority": "high"},
            {"role": "hats", "type": "drum", "priority": "medium"},
            {"role": "bass", "type": "bass", "priority": "high"},
        ]
        
        if style == "inspired":
            base.extend([
                {"role": "melody", "type": "synth", "priority": "medium"},
                {"role": "pad", "type": "synth", "priority": "low"},
            ])
        elif style == "similar":
            base.extend([
                {"role": "lead", "type": "synth", "priority": "high"},
                {"role": "arp", "type": "synth", "priority": "medium"},
                {"role": "fx", "type": "fx", "priority": "low"},
            ])
        elif style == "remix":
            base.extend([
                {"role": "seed_chops", "type": "sampler", "priority": "high"},
                {"role": "stutter_fx", "type": "fx", "priority": "medium"},
                {"role": "vocal_chops", "type": "sampler", "priority": "medium"},
            ])
        
        return base
    
    def create_vocal_chops(self, vocal_sample_path: str,
                          num_chops: int = 8) -> Dict[str, Any]:
        """
        T055: Crea vocal chops y los mapea a Drum Rack.
        
        Args:
            vocal_sample_path: Ruta al sample vocal
            num_chops: Número de chops a crear
            
        Returns:
            Dict con chops generados y mapeo a pads
        """
        if not os.path.exists(vocal_sample_path):
            return {"error": f"Vocal sample no encontrado: {vocal_sample_path}"}
        
        positions = [i / num_chops + random.uniform(-0.05, 0.05) for i in range(num_chops)]
        
        chops = []
        for i, pos in enumerate(positions):
            chops.append({
                "chop_index": i,
                "pad_note": 36 + i,
                "start_position": pos,
                "duration": 0.5,
                "transient_strength": random.uniform(0.5, 1.0),
            })
        
        pattern = []
        for i in range(8):
            pattern.append({
                "note": 36 + (i % num_chops),
                "start_time": i * 0.5,
                "velocity": 100 if i % 4 == 0 else 80,
            })
        
        return {
            "source_sample": vocal_sample_path,
            "num_chops": len(chops),
            "chops": chops,
            "drum_rack_mapping": {
                "base_note": 36,
                "note_range": f"36-{36 + len(chops) - 1}",
            },
            "suggested_pattern": pattern,
        }


# =============================================================================
# PARTE 4 - Referencia y Comparación (T056-T060)
# =============================================================================

class ReferenceMatcher:
    """
    Compara proyectos con referencias profesionales y adapta.
    
    Métodos:
    - T056: match_reference_energy() - Ajusta energía
    - T057: match_reference_spectrum() - Ajusta EQ
    - T058: match_reference_width() - Ajusta stereo width
    - T059: generate_similarity_report() - Score por dimensión
    - T060: adapt_to_reference_style() - Adapta estructura e instrumentación
    """
    
    def match_reference_energy(self, project_tracks: List[Dict[str, Any]],
                              reference_energy_curve: EnergyCurve) -> Dict[str, Any]:
        """
        T056: Ajusta la energía del proyecto para coincidir con referencia.
        
        Args:
            project_tracks: Tracks del proyecto actual
            reference_energy_curve: Curva de energía de referencia
            
        Returns:
            Dict con ajustes sugeridos
        """
        current = self._analyze_project_energy(project_tracks)
        adjustments = []
        
        for i, (bar, target) in enumerate(zip(reference_energy_curve.bars,
                                               reference_energy_curve.levels)):
            cur = current.get_level_at(bar)
            diff = target - cur
            
            if abs(diff) > 0.1:
                adjustments.append({
                    "bar": bar,
                    "section": reference_energy_curve.section_names[i] if i < len(reference_energy_curve.section_names) else "unknown",
                    "target_energy": round(target, 2),
                    "current_energy": round(cur, 2),
                    "adjustment": round(diff, 2),
                    "suggestion": self._energy_suggestion(diff),
                })
        
        return {
            "reference_curve": reference_energy_curve.to_dict(),
            "current_curve": current.to_dict(),
            "adjustments_needed": len(adjustments),
            "adjustments": adjustments,
            "overall_match_score": self._curve_similarity(current, reference_energy_curve),
        }
    
    def _analyze_project_energy(self, tracks: List[Dict[str, Any]]) -> EnergyCurve:
        bars, levels = [], []
        
        for bar in range(0, 64, 4):
            energy = sum(
                (np.mean([n.get('velocity', 100) for n in t.get('notes', []) if bar <= n.get('start_time', 0) < bar + 4] or [0]) / 127.0) *
                min(1.0, len([n for n in t.get('notes', []) if bar <= n.get('start_time', 0) < bar + 4]) / 16)
                for t in tracks
            ) / max(len(tracks), 1)
            bars.append(bar)
            levels.append(min(1.0, energy))
        
        return EnergyCurve(bars=bars, levels=levels)
    
    def _energy_suggestion(self, diff: float) -> str:
        if diff > 0.3:
            return "Añadir capas de drums y subir volumen general"
        elif diff > 0.15:
            return "Aumentar elementos percusivos o volumen de drums"
        elif diff > 0:
            return "Subir ligeramente volumen de elementos principales"
        elif diff < -0.3:
            return "Reducir drásticamente densidad de tracks"
        elif diff < -0.15:
            return "Bajar volumen de pads/synths"
        return "Ajuste fino de balance"
    
    def _curve_similarity(self, c1: EnergyCurve, c2: EnergyCurve) -> float:
        min_len = min(len(c1.levels), len(c2.levels))
        if min_len < 2:
            return 0.5
        corr = np.corrcoef(np.array(c1.levels[:min_len]), np.array(c2.levels[:min_len]))[0, 1]
        return round((corr + 1) / 2, 3) if not np.isnan(corr) else 0.5
    
    def match_reference_spectrum(self, project_eq: Dict[str, Any],
                                reference_spectrum: SpectrumProfile) -> Dict[str, Any]:
        """
        T057: Compara y ajusta EQ para coincidir con referencia.
        
        Args:
            project_eq: EQ actual del proyecto
            reference_spectrum: Perfil espectral de referencia
            
        Returns:
            Dict con recomendaciones de EQ
        """
        current = project_eq.get('bands', {})
        bands = [
            ('low', reference_spectrum.low_energy, current.get('low', 0.5)),
            ('low_mid', reference_spectrum.low_mid_energy, current.get('low_mid', 0.5)),
            ('mid', reference_spectrum.mid_energy, current.get('mid', 0.5)),
            ('high_mid', reference_spectrum.high_mid_energy, current.get('high_mid', 0.5)),
            ('high', reference_spectrum.high_energy, current.get('high', 0.5)),
        ]
        
        eq_adj = []
        for name, target, cur in bands:
            diff = target - cur
            if abs(diff) > 0.05:
                eq_adj.append({
                    "band": name,
                    "target_db": round(target * 12 - 6, 1),
                    "current_db": round(cur * 12 - 6, 1),
                    "adjustment_db": round(diff * 12, 1),
                    "action": "boost" if diff > 0 else "cut",
                })
        
        distance = np.linalg.norm(np.array([b[1] for b in bands]) - np.array([b[2] for b in bands]))
        
        return {
            "reference_spectrum": reference_spectrum.to_dict(),
            "current_eq": project_eq,
            "eq_adjustments": eq_adj,
            "spectrum_match_score": round(max(0, 1 - distance / 2), 3),
            "needs_eq_work": len(eq_adj) > 2,
        }
    
    def match_reference_width(self, project_stereo: Dict[str, Any],
                             reference_width: StereoWidth) -> Dict[str, Any]:
        """
        T058: Compara y ajusta ancho estéreo para coincidir con referencia.
        
        Args:
            project_stereo: Ancho estéreo actual del proyecto
            reference_width: Ancho estéreo de referencia
            
        Returns:
            Dict con recomendaciones de ancho estéreo
        """
        current = StereoWidth(
            low=project_stereo.get('low', 0.1),
            mid_low=project_stereo.get('mid_low', 0.3),
            mid=project_stereo.get('mid', 0.5),
            high=project_stereo.get('high', 0.7),
        )
        
        comps = [
            ("low", current.low, reference_width.low, 0.2),
            ("mid_low", current.mid_low, reference_width.mid_low, 0.4),
            ("mid", current.mid, reference_width.mid, 0.5),
            ("high", current.high, reference_width.high, 0.6),
        ]
        
        width_adj = []
        for band, cur, ref, tol in comps:
            diff = cur - ref
            if abs(diff) > tol:
                width_adj.append({
                    "band": band,
                    "current_width": round(cur, 2),
                    "reference_width": round(ref, 2),
                    "difference": round(diff, 2),
                    "action": "narrow" if diff > 0 else "widen",
                    "suggestion": self._width_suggestion(band, diff),
                })
        
        match_score = max(0, 1 - np.mean([abs(c[1] - c[2]) for c in comps]))
        
        return {
            "reference_width": reference_width.to_dict(),
            "current_width": current.to_dict(),
            "width_adjustments": width_adj,
            "width_match_score": round(match_score, 3),
            "is_balanced": current.is_balanced(),
        }
    
    def _width_suggestion(self, band: str, diff: float) -> str:
        if band == "low":
            return "Usar Utility o EQ para mono en frecuencias bajas" if diff > 0 else "Más mono en bajos mejora potencia"
        elif band == "high":
            return "Añadir chorus o delay corto para ampliar agudos" if diff < 0 else "Más estrecho para evitar perder foco"
        return "Considerar paneo más amplio en rango medio" if diff < 0 else "Más estrecho para mejor cohesión"
    
    def generate_similarity_report(self, project: Dict[str, Any],
                                  reference: Dict[str, Any]) -> Dict[str, Any]:
        """
        T059: Genera reporte detallado de similitud por dimensiones.
        
        Args:
            project: Datos del proyecto actual
            reference: Datos de la referencia
            
        Returns:
            Dict con SimilarityScore desglosado
        """
        scores = SimilarityScore()
        
        bpm_diff = abs(project.get('tempo', 120) - reference.get('tempo', 120))
        scores.bpm_score = max(0, 1 - (bpm_diff / 30))
        
        p_key, r_key = project.get('key', ''), reference.get('key', '')
        scores.key_score = 1.0 if p_key == r_key else (0.5 if p_key and r_key and p_key[0] == r_key[0] else 0.0)
        
        p_energy, r_energy = project.get('energy_curve', {}), reference.get('energy_curve', {})
        if p_energy and r_energy:
            p_l, r_l = p_energy.get('levels', []), r_energy.get('levels', [])
            if p_l and r_l:
                min_len = min(len(p_l), len(r_l))
                corr = np.corrcoef(p_l[:min_len], r_l[:min_len])[0, 1]
                scores.energy_score = (corr + 1) / 2 if not np.isnan(corr) else 0.5
        
        p_spec, r_spec = project.get('spectrum', {}), reference.get('spectrum', {})
        if p_spec and r_spec:
            distance = np.linalg.norm(
                np.array([p_spec.get(k, 0) for k in ['low', 'mid', 'high']]) -
                np.array([r_spec.get(k, 0) for k in ['low', 'mid', 'high']])
            )
            scores.spectrum_score = max(0, 1 - distance / 3)
        
        p_width, r_width = project.get('stereo_width', {}), reference.get('stereo_width', {})
        if p_width and r_width:
            diffs = [abs(p_width.get(k, 0) - r_width.get(k, 0)) for k in ['low', 'mid', 'high']]
            scores.width_score = max(0, 1 - np.mean(diffs))
        
        total = scores.total
        interpretation = (
            "Muy similar" if total >= 0.85 else
            "Similar" if total >= 0.70 else
            "Moderadamente similar" if total >= 0.55 else
            "Poco similar" if total >= 0.40 else
            "Diferente"
        )
        
        return {
            "similarity_scores": scores.to_dict(),
            "total_similarity": total,
            "interpretation": interpretation,
            "dimension_analysis": {
                "bpm": {"project": project.get('tempo', 0), "reference": reference.get('tempo', 0), "score": scores.bpm_score},
                "key": {"project": p_key, "reference": r_key, "score": scores.key_score},
                "energy": {"score": scores.energy_score},
                "spectrum": {"score": scores.spectrum_score},
                "width": {"score": scores.width_score},
            },
        }
    
    def adapt_to_reference_style(self, project: Dict[str, Any],
                                reference_style: str) -> Dict[str, Any]:
        """
        T060: Adapta estructura e instrumentación al estilo de referencia.
        
        Args:
            project: Proyecto a adaptar
            reference_style: Estilo de referencia ('pop', 'edm', 'hiphop', 'reggaeton')
            
        Returns:
            Dict con adaptaciones sugeridas
        """
        profiles = {
            'reggaeton': {
                'structure': ['intro', 'verse', 'build', 'drop', 'break', 'drop', 'outro'],
                'bpm_range': (85, 105),
                'key_type': 'minor',
                'instruments': ['kick', 'snare', 'dembow_hats', 'bass', 'synth_lead'],
                'width': 'narrow_low_wide_high',
            },
            'pop': {
                'structure': ['intro', 'verse', 'prechorus', 'chorus', 'verse', 'chorus', 'bridge', 'chorus', 'outro'],
                'bpm_range': (90, 130),
                'key_type': 'major',
                'instruments': ['kick', 'snare', 'hats', 'bass', 'pad', 'lead_vocal'],
                'width': 'balanced',
            },
            'edm': {
                'structure': ['intro', 'build', 'drop', 'break', 'build', 'drop', 'outro'],
                'bpm_range': (120, 140),
                'key_type': 'minor',
                'instruments': ['kick', 'snare', 'hats', 'sub_bass', 'synth_lead', 'fx'],
                'width': 'wide',
            },
            'hiphop': {
                'structure': ['intro', 'verse', 'hook', 'verse', 'hook', 'bridge', 'hook', 'outro'],
                'bpm_range': (70, 100),
                'key_type': 'minor',
                'instruments': ['kick', 'snare', 'hats', '808_bass', 'sample', 'vocal'],
                'width': 'centered',
            },
        }
        
        profile = profiles.get(reference_style.lower(), profiles['reggaeton'])
        current_tracks = project.get('tracks', [])
        current_bpm = project.get('tempo', 120)
        current_roles = {t.get('role', 'unknown') for t in current_tracks}
        
        changes = [
            {"action": "add", "instrument": i, "reason": "Característico del estilo"}
            for i in profile['instruments'] if i not in current_roles
        ]
        changes.extend([
            {"action": "consider_remove", "instrument": r, "reason": "No típico del estilo"}
            for r in current_roles if r not in profile['instruments']
        ])
        
        priorities = []
        if not (profile['bpm_range'][0] <= current_bpm <= profile['bpm_range'][1]):
            priorities.append("adjust_bpm")
        if len(project.get('structure', [])) < len(profile['structure']):
            priorities.append("extend_structure")
        if [i for i in profile['instruments'] if i not in current_roles]:
            priorities.append("add_missing_instruments")
        if not priorities:
            priorities.append("fine_tune_mix")
        
        return {
            "target_style": reference_style,
            "current_structure": project.get('structure', []),
            "suggested_structure": profile['structure'],
            "bpm_adjustment": {
                "current": current_bpm,
                "target_range": profile['bpm_range'],
                "suggested": sum(profile['bpm_range']) // 2,
            },
            "instrumentation_changes": changes,
            "stereo_width_target": profile['width'],
            "adaptation_priority": priorities,
        }


# =============================================================================
# AGENTE 13 - EXTENDED CHORDS ENGINE (Acordes Ricos)
# =============================================================================

# Extended chord structures with intervals (semitones from root)
CHORD_STRUCTURES = {
    # Basic triads and sevenths (existing)
    'maj': [0, 4, 7],
    'min': [0, 3, 7],
    'dim': [0, 3, 6],
    'aug': [0, 4, 8],
    'maj7': [0, 4, 7, 11],
    'min7': [0, 3, 7, 10],
    'dom7': [0, 4, 7, 10],
    'dim7': [0, 3, 6, 9],
    'half_dim': [0, 3, 6, 10],
    'min_maj7': [0, 3, 7, 11],
    
    # 9ths
    'maj9': [0, 4, 7, 11, 14],      # 1, 3, 5, 7, 9
    'min9': [0, 3, 7, 10, 14],      # 1, b3, 5, b7, 9
    'dom9': [0, 4, 7, 10, 14],      # 1, 3, 5, b7, 9
    '9': [0, 4, 7, 10, 14],         # Alias for dom9
    'maj_add9': [0, 4, 7, 14],      # 1, 3, 5, 9 (no 7th)
    'min_add9': [0, 3, 7, 14],      # 1, b3, 5, 9 (no 7th)
    
    # 11ths
    'maj11': [0, 4, 7, 11, 14, 17], # 1, 3, 5, 7, 9, 11
    'min11': [0, 3, 7, 10, 14, 17], # 1, b3, 5, b7, 9, 11
    'dom11': [0, 4, 7, 10, 14, 17], # 1, 3, 5, b7, 9, 11
    '11': [0, 4, 7, 10, 14, 17],    # Alias for dom11
    
    # 13ths
    'maj13': [0, 4, 7, 11, 14, 17, 21],  # 1, 3, 5, 7, 9, 11, 13
    'min13': [0, 3, 7, 10, 14, 17, 21],  # 1, b3, 5, b7, 9, 11, 13
    'dom13': [0, 4, 7, 10, 14, 17, 21],  # 1, 3, 5, b7, 9, 11, 13
    '13': [0, 4, 7, 10, 14, 17, 21],     # Alias for dom13
    
    # Sus chords
    'sus2': [0, 2, 7],              # 1, 2, 5
    'sus4': [0, 5, 7],              # 1, 4, 5
    '7sus4': [0, 5, 7, 10],         # 1, 4, 5, b7
    '9sus4': [0, 5, 7, 10, 14],     # 1, 4, 5, b7, 9
    
    # Altered dominant chords
    '7b5': [0, 4, 6, 10],           # 1, 3, b5, b7
    '7b9': [0, 4, 7, 10, 13],       # 1, 3, 5, b7, b9
    '7#9': [0, 4, 7, 10, 15],       # 1, 3, 5, b7, #9
    '7#11': [0, 4, 7, 10, 18],      # 1, 3, 5, b7, #11
    '7b13': [0, 4, 7, 10, 20],      # 1, 3, 5, b7, b13
    'alt': [0, 4, 6, 10, 13, 20],   # Altered - 1, 3, b5, b7, b9, b13
}

# Chord type categories for UI/selection
CHORD_CATEGORIES = {
    'triads': ['maj', 'min', 'dim', 'aug'],
    'sevenths': ['maj7', 'min7', 'dom7', 'dim7', 'half_dim', 'min_maj7'],
    'ninths': ['maj9', 'min9', 'dom9', '9', 'maj_add9', 'min_add9'],
    'elevenths': ['maj11', 'min11', 'dom11', '11'],
    'thirteenths': ['maj13', 'min13', 'dom13', '13'],
    'suspended': ['sus2', 'sus4', '7sus4', '9sus4'],
    'altered': ['7b5', '7b9', '7#9', '7#11', '7b13', 'alt'],
}

# Root note to MIDI mapping (C4 = 60)
NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']

def parse_chord_name(chord_name: str) -> Tuple[str, int]:
    """Parse a chord name like 'Cmaj9' or 'Am7' into (root_note, quality).
    
    Returns:
        Tuple of (root_note_name, root_midi_number)
    """
    chord_name = chord_name.strip()
    
    # Determine root note (handle sharps and flats)
    root = None
    root_idx = 0
    if len(chord_name) > 1 and chord_name[1] in '#b':
        root = chord_name[:2]
        quality = chord_name[2:]
    else:
        root = chord_name[0].upper()
        quality = chord_name[1:]
    
    # Convert root to MIDI note number (C4 = 60)
    root_clean = root.replace('b', '-').replace('#', '+')
    try:
        root_idx = NOTE_NAMES.index(root.replace('b', '').replace('#', ''))
        if 'b' in root or '-' in root:
            root_idx = (root_idx - 1) % 12
        if '#' in root or '+' in root:
            root_idx = (root_idx + 1) % 12
    except ValueError:
        root_idx = 0
    
    # Default to C4 if no root found
    root_midi = 60 + root_idx  # C4
    
    return quality, root_midi


def get_chord_notes(chord_name: str, root_midi: int = None) -> List[int]:
    """Generate MIDI note numbers for a named chord.
    
    Args:
        chord_name: Chord name like 'maj9', 'min7', '7b9'
        root_midi: Root note MIDI number (default: 60 for C4)
    
    Returns:
        List of MIDI note numbers for the chord tones
    """
    if root_midi is None:
        quality, root_midi = parse_chord_name(chord_name)
    else:
        quality = chord_name
    
    # Normalize quality name
    quality = quality.lower().replace('-', 'min').replace('m7b5', 'half_dim')
    
    # Get intervals
    intervals = CHORD_STRUCTURES.get(quality, CHORD_STRUCTURES.get('maj'))
    
    # Calculate notes
    return [(root_midi + interval) % 128 for interval in intervals]


# =============================================================================
# VOICE LEADING FUNCTIONS
# =============================================================================

def drop_2_voicing(chord_notes: List[int]) -> List[int]:
    """Create a drop-2 voicing by lowering the second note from top by an octave.
    
    Drop-2 voicings are widely used in jazz and create smooth voice leading.
    The second note from the top is dropped down one octave.
    
    Args:
        chord_notes: List of MIDI notes (sorted high to low or low to high)
    
    Returns:
        Re-voiced chord with drop-2 spacing
    """
    if len(chord_notes) < 3:
        return chord_notes[:]
    
    # Sort from high to low
    sorted_notes = sorted(chord_notes, reverse=True)
    
    # Drop the second note from top down an octave
    voicing = sorted_notes[:]
    voicing[1] = voicing[1] - 12  # Drop down one octave
    
    # Re-sort to keep proper order
    return sorted(voicing)


def drop_3_voicing(chord_notes: List[int]) -> List[int]:
    """Create a drop-3 voicing by lowering the third note from top by an octave.
    
    Drop-3 voicings have a wider spacing between the lowest and highest notes,
    creating an open, airy sound.
    
    Args:
        chord_notes: List of MIDI notes
    
    Returns:
        Re-voiced chord with drop-3 spacing
    """
    if len(chord_notes) < 4:
        return drop_2_voicing(chord_notes)
    
    # Sort from high to low
    sorted_notes = sorted(chord_notes, reverse=True)
    
    # Drop the third note from top down an octave
    voicing = sorted_notes[:]
    voicing[2] = voicing[2] - 12
    
    # Re-sort
    return sorted(voicing)


def open_voicing(chord_notes: List[int], spread: int = 12) -> List[int]:
    """Create an open voicing by spreading chord tones across octaves.
    
    This creates a wide, orchestral sound by distributing notes.
    
    Args:
        chord_notes: List of MIDI notes
        spread: Semitones to spread (default 12 = one octave)
    
    Returns:
        Re-voiced chord with open spacing
    """
    if len(chord_notes) < 2:
        return chord_notes[:]
    
    # Keep bass note, spread others upward
    bass = min(chord_notes)
    others = sorted([n for n in chord_notes if n != bass])
    
    result = [bass]
    for i, note in enumerate(others):
        # Spread each successive note higher
        spread_note = note + (i * spread // len(others))
        # Keep within MIDI range
        while spread_note > 127:
            spread_note -= 12
        result.append(spread_note)
    
    return sorted(result)


def minimal_movement(current_chord: List[int], next_chord: List[int]) -> List[int]:
    """Optimize voice leading between two chords for minimal movement.
    
    This finds the closest voicing of next_chord to current_chord,
    minimizing the total distance between voices.
    
    Args:
        current_chord: Current chord notes (list of MIDI numbers)
        next_chord: Target chord notes (list of MIDI numbers)
    
    Returns:
        Optimized next chord with minimal voice movement
    """
    if not current_chord or not next_chord:
        return next_chord[:]
    
    current_sorted = sorted(current_chord)
    next_sorted = sorted(next_chord)
    
    # Try different octave transpositions for each note in next_chord
    # to find the configuration with minimal total movement
    best_voicing = next_sorted[:]
    best_distance = float('inf')
    
    # Generate possible octave shifts for each note
    def get_octave_options(note, target_note):
        """Get the note shifted by different octaves near the target."""
        options = []
        for shift in [-24, -12, 0, 12, 24]:
            shifted = note + shift
            if 0 <= shifted <= 127:
                options.append(shifted)
        return options
    
    # For smaller chords, try all combinations
    if len(next_sorted) <= 4:
        from itertools import product
        
        options_per_note = [get_octave_options(n, current_sorted[0]) for n in next_sorted]
        
        for combination in product(*options_per_note):
            # Calculate total distance to current chord
            # Match voices (lower to lower, higher to higher)
            combo_sorted = sorted(combination)
            
            # Pad to match lengths if needed
            curr = current_sorted[:]
            nxt = combo_sorted[:]
            
            while len(curr) < len(nxt):
                curr.append(curr[-1] if curr else 60)
            while len(nxt) < len(curr):
                nxt.append(nxt[-1] if nxt else 60)
            
            distance = sum(abs(c - n) for c, n in zip(curr, nxt))
            
            if distance < best_distance:
                best_distance = distance
                best_voicing = list(combination)
    else:
        # For larger chords, use greedy approach
        best_voicing = []
        for i, next_note in enumerate(next_sorted):
            target = current_sorted[min(i, len(current_sorted) - 1)]
            options = get_octave_options(next_note, target)
            best = min(options, key=lambda x: abs(x - target))
            best_voicing.append(best)
    
    return sorted(best_voicing)


def voice_chord_progression(chords: List[List[int]], 
                            voicing_type: str = "drop2") -> List[List[int]]:
    """Apply voice leading to an entire chord progression.
    
    Args:
        chords: List of chord note lists
        voicing_type: 'drop2', 'drop3', 'open', or 'minimal'
    
    Returns:
        List of voiced chords with smooth voice leading
    """
    if not chords:
        return []
    
    voiced = []
    
    for i, chord in enumerate(chords):
        # Apply voicing type
        if voicing_type == "drop2":
            current = drop_2_voicing(chord)
        elif voicing_type == "drop3":
            current = drop_3_voicing(chord)
        elif voicing_type == "open":
            current = open_voicing(chord)
        elif voicing_type == "minimal" and i > 0:
            current = minimal_movement(voiced[-1], chord)
        else:
            current = sorted(chord)
        
        # If minimal movement, also apply to transitions
        if voicing_type == "minimal" and i > 0:
            current = minimal_movement(voiced[-1], current)
        
        voiced.append(current)
    
    return voiced


class ExtendedChordsEngine:
    """Engine for generating advanced chord voicings and progressions.
    
    Provides rich harmonic content with extended chords (9ths, 11ths, 13ths),
    suspended chords, and altered dominants. Includes intelligent voice leading.
    
    Features:
    - Extended chord generation (9ths, 11ths, 13ths)
    - Suspended and altered chords
    - Jazz-style voice leading (drop-2, drop-3, open voicings)
    - Smooth voice movement between chords
    """
    
    def __init__(self):
        self.chord_structures = CHORD_STRUCTURES
        self.categories = CHORD_CATEGORIES
    
    def generate_extended_chord(self, root: str, chord_type: str, 
                                 octave: int = 4, voicing: str = "default") -> Dict[str, Any]:
        """Generate an extended chord with specified voicing.
        
        Args:
            root: Root note (e.g., 'C', 'F#', 'Bb')
            chord_type: Chord quality (e.g., 'maj9', 'min11', '7b9')
            octave: Octave number (4 = middle C)
            voicing: Voicing type ('default', 'drop2', 'drop3', 'open')
        
        Returns:
            Dict with chord notes, MIDI numbers, and metadata
        """
        # Parse root note to MIDI
        root_clean = root.upper()
        if len(root_clean) > 1 and root_clean[1] == 'B':
            root_clean = root_clean[0] + 'b'
        if len(root_clean) > 1 and root_clean[1] == '#':
            root_clean = root_clean[0] + '#'
        
        # Get root index
        base_note = root_clean[0]
        try:
            root_idx = NOTE_NAMES.index(base_note)
        except ValueError:
            root_idx = 0
        
        # Apply accidentals
        if 'b' in root_clean or '♭' in root_clean:
            root_idx = (root_idx - 1) % 12
        if '#' in root_clean or '♯' in root_clean:
            root_idx = (root_idx + 1) % 12
        
        # Calculate root MIDI note
        root_midi = (octave + 1) * 12 + root_idx  # C4 = 60
        
        # Normalize chord type
        chord_type = chord_type.lower().replace('-', 'min').replace('major', 'maj')
        
        # Get chord intervals
        intervals = self.chord_structures.get(chord_type)
        if intervals is None:
            # Try to find similar
            for key in self.chord_structures:
                if chord_type in key or key in chord_type:
                    intervals = self.chord_structures[key]
                    chord_type = key
                    break
            if intervals is None:
                intervals = self.chord_structures['maj']
                chord_type = 'maj'
        
        # Generate notes
        notes = [(root_midi + interval) % 128 for interval in intervals]
        
        # Apply voicing
        if voicing == "drop2":
            notes = drop_2_voicing(notes)
        elif voicing == "drop3":
            notes = drop_3_voicing(notes)
        elif voicing == "open":
            notes = open_voicing(notes)
        
        # Generate note names
        note_names = []
        for note in notes:
            note_idx = note % 12
            octave_num = (note // 12) - 1
            note_names.append(f"{NOTE_NAMES[note_idx]}{octave_num}")
        
        return {
            "root": root,
            "chord_type": chord_type,
            "voicing": voicing,
            "octave": octave,
            "midi_notes": notes,
            "note_names": note_names,
            "intervals": intervals,
            "category": self._get_category(chord_type),
        }
    
    def _get_category(self, chord_type: str) -> str:
        """Get the category for a chord type."""
        for cat, types in self.categories.items():
            if chord_type in types:
                return cat
        return "other"
    
    def generate_chord_progression(self, roots: List[str], chord_types: List[str],
                                     voicing: str = "minimal") -> List[Dict[str, Any]]:
        """Generate a chord progression with smooth voice leading.
        
        Args:
            roots: List of root notes
            chord_types: List of chord types (parallel to roots)
            voicing: Voice leading type ('minimal', 'drop2', 'drop3', 'open')
        
        Returns:
            List of chord dictionaries with voiced notes
        """
        # Generate basic chords
        chords = []
        for root, ctype in zip(roots, chord_types):
            chord = self.generate_extended_chord(root, ctype, voicing="default")
            chords.append(chord)
        
        # Apply voice leading
        if voicing == "minimal" and len(chords) > 1:
            # Apply minimal movement between successive chords
            for i in range(1, len(chords)):
                prev_notes = chords[i-1]["midi_notes"]
                curr_notes = chords[i]["midi_notes"]
                voiced = minimal_movement(prev_notes, curr_notes)
                chords[i]["midi_notes"] = voiced
                # Update note names
                chords[i]["note_names"] = [
                    f"{NOTE_NAMES[n % 12]}{(n // 12) - 1}" for n in voiced
                ]
        elif voicing in ("drop2", "drop3", "open"):
            for chord in chords:
                notes = chord["midi_notes"]
                if voicing == "drop2":
                    notes = drop_2_voicing(notes)
                elif voicing == "drop3":
                    notes = drop_3_voicing(notes)
                elif voicing == "open":
                    notes = open_voicing(notes)
                chord["midi_notes"] = notes
                chord["voicing"] = voicing
                chord["note_names"] = [
                    f"{NOTE_NAMES[n % 12]}{(n // 12) - 1}" for n in notes
                ]
        
        return chords
    
    def get_available_chord_types(self, category: str = None) -> List[str]:
        """Get list of available chord types, optionally filtered by category.
        
        Args:
            category: Optional category filter ('ninths', 'elevenths', etc.)
        
        Returns:
            List of chord type names
        """
        if category and category in self.categories:
            return self.categories[category][:]
        
        # Return all types
        all_types = []
        for types in self.categories.values():
            all_types.extend(types)
        return sorted(set(all_types))
    
    def suggest_chords_for_key(self, key: str) -> Dict[str, List[str]]:
        """Suggest appropriate chords for a given key.
        
        Args:
            key: Musical key (e.g., 'C', 'Am', 'F#')
        
        Returns:
            Dict with chord suggestions by degree
        """
        # Parse key - detect minor BEFORE upper-casing to preserve 'm' suffix
        original_key = key.strip()
        is_minor = original_key.endswith('m') or original_key.endswith('min') or original_key.endswith('minor')
        # Also check for lowercase single-letter keys (e.g., 'a' = A minor convention)
        if not is_minor and len(original_key) == 1 and original_key.islower():
            is_minor = True
        
        key = original_key.upper()
        base = key[0]
        if len(key) > 1 and key[1] in '#B':
            base = key[:2]
        
        # Determine scale degrees
        if is_minor:
            # Natural minor scale degrees
            degrees = ['min', 'dim', 'maj', 'min', 'min', 'maj', 'maj']
            extensions = ['min7', 'half_dim', 'maj7', 'min7', 'min7', 'maj7', '7']
        else:
            # Major scale degrees
            degrees = ['maj', 'min', 'min', 'maj', 'maj', 'min', 'dim']
            extensions = ['maj7', 'min7', 'min7', 'maj7', '7', 'min7', 'half_dim']
        
        # Extended jazz voicings
        extended_minor = ['min9', 'min11', 'min6', 'min9', 'min11', 'maj9', '7b9']
        extended_major = ['maj9', 'min9', 'min11', 'maj13', '13', 'min9', '7alt']
        
        roman = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII']
        if is_minor:
            roman = ['i', 'ii°', 'III', 'iv', 'v', 'VI', 'VII']
        
        return {
            "key": key,
            "is_minor": is_minor,
            "basic": [f"{base}{deg}" for deg in degrees],
            "sevenths": [f"{base}{ext}" for ext in extensions],
            "extended": extended_minor if is_minor else extended_major,
            "roman_numerals": roman,
        }
    
    # Fase 47: Inversiones
    def invert_chord(self, notes: List[int], inversion: int = 0) -> List[int]:
        """Apply inversion to a chord.
        
        Args:
            notes: List of MIDI notes in the chord
            inversion: Inversion level (0=root position, 1=1st inversion,
                      2=2nd inversion, 3=3rd inversion)
        
        Returns:
            List of inverted MIDI notes
            
        Fase 47: Inversiones
        - inversion=0: root position (posición fundamental)
        - inversion=1: primera inversión (tercera en el bajo)
        - inversion=2: segunda inversión (quinta en el bajo)  
        - inversion=3: tercera inversión (séptima en el bajo)
        """
        if not notes:
            return notes
        
        inversion = inversion % len(notes)  # Normalize
        if inversion == 0:
            return sorted(notes)
        
        # Rotate notes
        inverted = notes[inversion:] + notes[:inversion]
        
        # Transpose rotated notes up an octave for close voicing
        result = []
        for i, note in enumerate(inverted):
            if i < inversion:
                # Notes that moved to bass, transpose up an octave
                result.append(note + 12)
            else:
                result.append(note)
        
        return sorted(result)
    
    # Fase 49: Chord Anticipation
    def apply_chord_anticipation(self, chord_start: float, tension: float,
                                   anticipation_amount: float = 0.25) -> float:
        """Apply chord anticipation based on tension level.
        
        Args:
            chord_start: Original chord position in beats
            tension: Tension level 0.0-1.0
            anticipation_amount: Anticipation amount in beats (default 1/16 = 0.25)
        
        Returns:
            New chord position (anticipated if tension > 0.6)
            
        Fase 49: Chord Anticipation
        En transiciones tensas (tension > 0.6), mover acorde 1/16 adelante del beat.
        """
        if tension > 0.6:
            return max(0, chord_start - anticipation_amount)
        return chord_start
    
    def select_chord_for_tension(self, tension: float, base_quality: str = "major") -> str:
        """Select extended chord type based on tension level.
        
        Args:
            tension: Tension level 0.0-1.0
            base_quality: Base quality (major/minor)
        
        Returns:
            Recommended extended chord type
        """
        import random
        
        # Map tension to chord categories
        if tension < 0.3:
            candidates = CHORD_CATEGORIES['suspended'] + ['maj_add9']
        elif tension < 0.6:
            candidates = CHORD_CATEGORIES['sevenths']
        elif tension < 0.8:
            candidates = CHORD_CATEGORIES['ninths'] + CHORD_CATEGORIES['suspended']
        else:
            candidates = (CHORD_CATEGORIES['elevenths'] + 
                          CHORD_CATEGORIES['thirteenths'] + 
                          CHORD_CATEGORIES['altered'])
        
        # Filter by base quality if possible
        if base_quality == "minor":
            filtered = [c for c in candidates if 'min' in c or c in ['sus2', 'sus4', '7sus4']]
            if filtered:
                return random.choice(filtered)
        
        return random.choice(candidates) if candidates else 'maj7'
    
    def get_inversion_for_tension(self, tension: float) -> int:
        """Determine inversion level based on tension.
        
        Args:
            tension: Tension level 0.0-1.0
        
        Returns:
            Inversion level (0-3)
        """
        import random
        
        if tension < 0.3:
            return 0  # Root position - stable
        elif tension < 0.5:
            return random.choice([0, 1])  # Occasional 1st inversion
        elif tension < 0.7:
            return random.choice([1, 2])  # 2nd inversion
        else:
            return random.choice([2, 3])  # 3rd inversion - maximum tension


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

def analyze_project_key(tracks: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Función de conveniencia para analizar key de proyecto."""
    analyzer = ProjectAnalyzer()
    return analyzer.analyze_project_key(tracks)


def harmonize_track(track_index: int, chord_progression: List[str]) -> Dict[str, Any]:
    """Función de conveniencia para armonizar track."""
    analyzer = ProjectAnalyzer()
    return analyzer.harmonize_track(track_index, chord_progression)


def generate_counter_melody(main_melody_track: Dict[str, Any],
                           harmony_level: str = "thirds") -> Dict[str, Any]:
    """Función de conveniencia para generar contra-melodía."""
    generator = CounterMelodyGenerator()
    return generator.generate_counter_melody(main_melody_track, harmony_level)


def variate_loop(loop_clips: List[Dict[str, Any]],
                variation_intensity: float = 0.5) -> List[Dict[str, Any]]:
    """Función de conveniencia para variar loop."""
    engine = VariationEngine()
    return engine.variate_loop(loop_clips, variation_intensity)


def create_vocal_chops(vocal_sample_path: str, num_chops: int = 8) -> Dict[str, Any]:
    """Función de conveniencia para crear vocal chops."""
    intelligence = SampleIntelligence()
    return intelligence.create_vocal_chops(vocal_sample_path, num_chops)


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

__all__ = [
    # Dataclasses
    "EnergyCurve",
    "SpectrumProfile",
    "StereoWidth",
    "SimilarityScore",
    # Clases principales - Parte 1 (T041-T045)
    "ProjectAnalyzer",
    "CounterMelodyGenerator",
    # Clases principales - Parte 2 (T046-T050)
    "VariationEngine",
    # Clases principales - Parte 3 (T051-T055)
    "SampleIntelligence",
    # Clases principales - Parte 4 (T056-T060)
    "ReferenceMatcher",
    # Agente 13 - Extended Chords Engine
    "ExtendedChordsEngine",
    "CHORD_STRUCTURES",
    "CHORD_CATEGORIES",
    # Voice leading functions
    "drop_2_voicing",
    "drop_3_voicing",
    "open_voicing",
    "minimal_movement",
    "voice_chord_progression",
    "get_chord_notes",
    "parse_chord_name",
    # Funciones de conveniencia
    "analyze_project_key",
    "harmonize_track",
    "generate_counter_melody",
    "variate_loop",
    "create_vocal_chops",
]
