"""
T136-T150: Live Performance & Advanced Search Module
BLOQUE 1: Nuevas herramientas MCP para AbletonMCP-AI
"""

import json
import time
import random
import hashlib
from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple, Set
from dataclasses import dataclass, field
from collections import defaultdict, deque
import logging

logger = logging.getLogger("AbletonMCP-LivePerformance")

# ============================================================================
# T136: Advanced Sample Search con LUFS
# ============================================================================

@dataclass
class SampleSearchFilters:
    """Filtros avanzados para búsqueda de samples"""
    query: str = ""
    category: str = ""
    sample_type: str = ""
    key: str = ""
    bpm: float = 0.0
    bpm_tolerance: float = 5.0
    genres: List[str] = field(default_factory=list)
    tags: List[str] = field(default_factory=list)
    # T136: Filtros LUFS
    lufs_min: float = -20.0
    lufs_max: float = -8.0
    lufs_target: Optional[float] = None
    lufs_tolerance: float = 2.0
    # Filtros adicionales
    duration_min: float = 0.0
    duration_max: float = 0.0
    spectral_centroid_min: float = 0.0
    spectral_centroid_max: float = 0.0


class AdvancedSampleSearcher:
    """T136: Búsqueda avanzada de samples con filtrado LUFS"""
    
    def __init__(self, sample_manager=None):
        self.sample_manager = sample_manager
        self._spectral_cache_path = Path.home() / ".abletonmcp_ai" / "spectral_cache.json"
        self._spectral_cache: Dict[str, Any] = {}
        self._load_spectral_cache()
    
    def _load_spectral_cache(self):
        """Carga cache espectral desde disco"""
        try:
            if self._spectral_cache_path.exists():
                with open(self._spectral_cache_path, 'r', encoding='utf-8') as f:
                    self._spectral_cache = json.load(f)
                logger.info(f"✓ T137: Spectral cache cargado: {len(self._spectral_cache)} samples")
            else:
                self._spectral_cache = {}
        except Exception as e:
            logger.warning(f"⚠ Error cargando spectral cache: {e}")
            self._spectral_cache = {}
    
    def save_spectral_cache(self):
        """T137: Guarda cache espectral a disco"""
        try:
            self._spectral_cache_path.parent.mkdir(parents=True, exist_ok=True)
            with open(self._spectral_cache_path, 'w', encoding='utf-8') as f:
                json.dump(self._spectral_cache, f, indent=2)
            logger.info(f"✓ T137: Spectral cache guardado: {len(self._spectral_cache)} samples")
        except Exception as e:
            logger.warning(f"⚠ Error guardando spectral cache: {e}")
    
    def search_with_filters(self, filters: SampleSearchFilters, limit: int = 20) -> List[Dict[str, Any]]:
        """T136: Búsqueda con filtros avanzados incluyendo LUFS"""
        results = []
        
        # Buscar samples base
        candidates = self._get_candidate_samples(filters)
        
        for sample in candidates:
            score = 0.0
            matches = []
            
            # Filtro LUFS T136
            if filters.lufs_target is not None:
                sample_lufs = self._get_sample_lufs(sample.get('path', ''))
                if sample_lufs is not None:
                    lufs_diff = abs(sample_lufs - filters.lufs_target)
                    if lufs_diff <= filters.lufs_tolerance:
                        score += (1.0 - lufs_diff / filters.lufs_tolerance) * 100
                        matches.append(f"LUFS match: {sample_lufs:.1f} LUFS")
                    else:
                        continue  # Fuera de rango LUFS
            
            # Filtro rango LUFS
            sample_lufs = self._get_sample_lufs(sample.get('path', ''))
            if sample_lufs is not None:
                if not (filters.lufs_min <= sample_lufs <= filters.lufs_max):
                    continue
            
            # Filtro BPM
            if filters.bpm > 0 and sample.get('bpm', 0) > 0:
                bpm_diff = abs(sample['bpm'] - filters.bpm)
                if bpm_diff <= filters.bpm_tolerance:
                    score += (1.0 - bpm_diff / filters.bpm_tolerance) * 50
                    matches.append(f"BPM match")
            
            # Filtro Key
            if filters.key and sample.get('key'):
                if sample['key'].lower() == filters.key.lower():
                    score += 40
                    matches.append("Key match")
            
            # Filtro categoría/tipo
            if filters.category and sample.get('category') == filters.category:
                score += 30
                matches.append("Category match")
            
            # Tags
            sample_tags = sample.get('tags', [])
            if filters.tags:
                matching_tags = set(sample_tags) & set(filters.tags)
                score += len(matching_tags) * 10
                if matching_tags:
                    matches.append(f"Tags: {matching_tags}")
            
            results.append({
                **sample,
                'search_score': score,
                'match_reasons': matches
            })
        
        # Ordenar por score
        results.sort(key=lambda x: x['search_score'], reverse=True)
        return results[:limit]
    
    def _get_candidate_samples(self, filters: SampleSearchFilters) -> List[Dict[str, Any]]:
        """Obtiene candidatos base para filtrar"""
        # Simulación - en implementación real usaría el sample_manager
        return []
    
    def _get_sample_lufs(self, sample_path: str) -> Optional[float]:
        """Obtiene LUFS integrado del sample desde cache"""
        if sample_path in self._spectral_cache:
            return self._spectral_cache[sample_path].get('lufs_integrated')
        return None


# ============================================================================
# T138: Palette Lock Persistente
# ============================================================================

class PersistentPaletteLock:
    """T138: Sistema de palette lock con persistencia en disco"""
    
    def __init__(self):
        self._lock_path = Path.home() / ".abletonmcp_ai" / "palette_lock.json"
        self._current_lock: Dict[str, str] = {}
        self._load_lock()
    
    def _load_lock(self):
        """Carga palette lock desde disco"""
        try:
            if self._lock_path.exists():
                with open(self._lock_path, 'r', encoding='utf-8') as f:
                    self._current_lock = json.load(f)
                logger.info(f"✓ T138: Palette lock cargado: {self._current_lock}")
            else:
                self._current_lock = {}
        except Exception as e:
            logger.warning(f"⚠ Error cargando palette lock: {e}")
            self._current_lock = {}
    
    def save_lock(self, drums: Optional[str], bass: Optional[str], music: Optional[str]) -> Dict[str, str]:
        """Guarda palette lock"""
        lock_data = {}
        if drums:
            lock_data['drums'] = drums
        if bass:
            lock_data['bass'] = bass
        if music:
            lock_data['music'] = music
        
        self._current_lock = lock_data
        
        try:
            self._lock_path.parent.mkdir(parents=True, exist_ok=True)
            with open(self._lock_path, 'w', encoding='utf-8') as f:
                json.dump(lock_data, f, indent=2)
            logger.info(f"✓ T138: Palette lock guardado: {lock_data}")
        except Exception as e:
            logger.warning(f"⚠ Error guardando palette lock: {e}")
        
        return lock_data
    
    def get_lock(self) -> Dict[str, str]:
        """Obtiene palette lock actual"""
        return self._current_lock.copy()
    
    def clear_lock(self):
        """Limpia palette lock"""
        self._current_lock = {}
        try:
            if self._lock_path.exists():
                self._lock_path.unlink()
            logger.info("✓ T138: Palette lock eliminado")
        except Exception as e:
            logger.warning(f"⚠ Error eliminando palette lock: {e}")


# ============================================================================
# T139: Mini-Sets Encadenados
# ============================================================================

@dataclass
class MiniSet:
    """Representa un mini-set de 15-30 minutos"""
    id: str
    name: str
    genre: str
    bpm_range: Tuple[float, float]
    key: str
    energy_profile: List[float]  # 0-1 por sección
    duration_minutes: float = 20.0
    palette_lock: Dict[str, str] = field(default_factory=dict)
    transition_out: Optional[str] = None


class MiniSetChainer:
    """T139: Sistema para encadenar mini-sets"""
    
    def __init__(self):
        self._chain_path = Path.home() / ".abletonmcp_ai" / "miniset_chains.json"
        self._chains: Dict[str, List[str]] = {}  # chain_id -> [miniset_ids]
        self._minisets: Dict[str, MiniSet] = {}
        self._load_chains()
    
    def _load_chains(self):
        """Carga cadenas guardadas"""
        try:
            if self._chain_path.exists():
                with open(self._chain_path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    self._chains = data.get('chains', {})
                    # Reconstruir MiniSets
                    for ms_data in data.get('minisets', {}).values():
                        self._minisets[ms_data['id']] = MiniSet(**ms_data)
        except Exception as e:
            logger.warning(f"⚠ Error cargando chains: {e}")
    
    def create_miniset(self, genre: str, bpm: float, key: str, 
                       duration: float = 20.0) -> MiniSet:
        """Crea un nuevo mini-set"""
        ms_id = f"ms_{int(time.time())}_{random.randint(1000, 9999)}"
        
        miniset = MiniSet(
            id=ms_id,
            name=f"{genre.title()} Set - {int(bpm)} BPM",
            genre=genre,
            bpm_range=(bpm - 3, bpm + 3),
            key=key,
            energy_profile=[0.3, 0.5, 0.8, 1.0, 0.9, 0.6, 0.4],  # Arco energético
            duration_minutes=duration
        )
        
        self._minisets[ms_id] = miniset
        return miniset
    
    def chain_minisets(self, miniset_ids: List[str], 
                       transition_type: str = "smooth") -> Dict[str, Any]:
        """Encadena múltiples mini-sets"""
        chain_id = f"chain_{int(time.time())}"
        
        # Validar compatibilidad
        minisets = [self._minisets.get(mid) for mid in miniset_ids if mid in self._minisets]
        
        if len(minisets) < 2:
            return {"error": "Need at least 2 minisets to chain"}
        
        # Analizar transiciones
        transitions = []
        for i in range(len(minisets) - 1):
            current = minisets[i]
            next_ms = minisets[i + 1]
            
            # Analizar compatibilidad BPM
            bpm_compatible = self._check_bpm_compatibility(
                current.bpm_range, next_ms.bpm_range
            )
            
            # Analizar compatibilidad armónica
            key_compatible = self._check_key_compatibility(current.key, next_ms.key)
            
            transitions.append({
                "from": current.id,
                "to": next_ms.id,
                "bpm_compatible": bpm_compatible,
                "key_compatible": key_compatible,
                "transition_type": transition_type,
                "recommended_duration_bars": 8 if bpm_compatible else 16
            })
        
        self._chains[chain_id] = miniset_ids
        self._save_chains()
        
        return {
            "chain_id": chain_id,
            "minisets": [ms.id for ms in minisets],
            "transitions": transitions,
            "total_duration": sum(ms.duration_minutes for ms in minisets)
        }
    
    def _check_bpm_compatibility(self, bpm_range1: Tuple[float, float], 
                                  bpm_range2: Tuple[float, float]) -> bool:
        """Verifica si rangos de BPM son compatibles"""
        # Rangos se solapan o están dentro de ±6%
        overlap = (bpm_range1[0] <= bpm_range2[1] and bpm_range2[0] <= bpm_range1[1])
        if overlap:
            return True
        
        # Check ±6% range
        max1, min2 = bpm_range1[1], bpm_range2[0]
        diff_percent = abs(max1 - min2) / ((max1 + min2) / 2) * 100
        return diff_percent <= 6
    
    def _check_key_compatibility(self, key1: str, key2: str) -> Dict[str, Any]:
        """Verifica compatibilidad armónica usando círculo de quintas"""
        # Círculo de quintas simplificado
        circle = ["C", "G", "D", "A", "E", "B", "F#", "C#", "G#", "D#", "A#", "F"]
        
        try:
            idx1 = circle.index(key1.replace("m", "").replace("M", ""))
            idx2 = circle.index(key2.replace("m", "").replace("M", ""))
            distance = min((idx2 - idx1) % 12, (idx1 - idx2) % 12)
            
            compatibility_scores = {
                0: 1.0,   # Misma key
                1: 0.9,   # Quinta
                2: 0.7,   # Segunda
                3: 0.5,   # Tercera
                4: 0.3,   # Cuarta
                5: 0.2,   # Tritono
                6: 0.1    # Opuesta
            }
            
            return {
                "compatible": distance <= 2,
                "distance_semitones": distance * 7 % 12,
                "circle_fifths_distance": distance,
                "compatibility_score": compatibility_scores.get(distance, 0.0)
            }
        except ValueError:
            return {"compatible": False, "error": "Key not in circle"}
    
    def _save_chains(self):
        """Guarda cadenas a disco"""
        try:
            self._chain_path.parent.mkdir(parents=True, exist_ok=True)
            data = {
                'chains': self._chains,
                'minisets': {k: self._miniset_to_dict(v) for k, v in self._minisets.items()}
            }
            with open(self._chain_path, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2, default=str)
        except Exception as e:
            logger.warning(f"⚠ Error guardando chains: {e}")
    
    def _miniset_to_dict(self, ms: MiniSet) -> Dict[str, Any]:
        """Convierte MiniSet a dict serializable"""
        return {
            'id': ms.id,
            'name': ms.name,
            'genre': ms.genre,
            'bpm_range': ms.bpm_range,
            'key': ms.key,
            'energy_profile': ms.energy_profile,
            'duration_minutes': ms.duration_minutes,
            'palette_lock': ms.palette_lock,
            'transition_out': ms.transition_out
        }


# ============================================================================
# T140: Transiciones Fluidas DJ entre BPMs diferentes
# ============================================================================

class DJTransitionEngine:
    """T140: Motor de transiciones fluidas entre clips de diferentes BPM"""
    
    def __init__(self):
        self._tempo_transition_path = Path.home() / ".abletonmcp_ai" / "tempo_transitions.json"
    
    def calculate_tempo_transition(self, bpm_from: float, bpm_to: float, 
                                    duration_bars: int = 8) -> Dict[str, Any]:
        """Calcula curva de transición de tempo"""
        
        # Estrategias de transición
        diff_percent = abs(bpm_to - bpm_from) / bpm_from * 100
        
        if diff_percent <= 3:
            # Diferencia pequeña: transición lineal simple
            curve_type = "linear"
            steps = self._linear_transition(bpm_from, bpm_to, duration_bars)
        elif diff_percent <= 6:
            # Diferencia media: curva suave S
            curve_type = "s_curve"
            steps = self._s_curve_transition(bpm_from, bpm_to, duration_bars)
        else:
            # Diferencia grande: transición en dos etapas con break
            curve_type = "break_transition"
            steps = self._break_transition(bpm_from, bpm_to, duration_bars)
        
        return {
            "curve_type": curve_type,
            "bpm_from": bpm_from,
            "bpm_to": bpm_to,
            "diff_percent": diff_percent,
            "duration_bars": duration_bars,
            "steps": steps,
            "recommended_effects": self._get_transition_effects(diff_percent)
        }
    
    def _linear_transition(self, bpm_from: float, bpm_to: float, 
                         bars: int) -> List[Dict[str, Any]]:
        """Transición lineal simple"""
        steps = []
        for i in range(bars + 1):
            progress = i / bars
            bpm = bpm_from + (bpm_to - bpm_from) * progress
            steps.append({
                "bar": i,
                "bpm": round(bpm, 2),
                "progress": round(progress, 2)
            })
        return steps
    
    def _s_curve_transition(self, bpm_from: float, bpm_to: float, 
                          bars: int) -> List[Dict[str, Any]]:
        """Transición con curva S suave"""
        import math
        steps = []
        for i in range(bars + 1):
            progress = i / bars
            # Curva sigmoide suave
            sigmoid = 1 / (1 + math.exp(-10 * (progress - 0.5)))
            bpm = bpm_from + (bpm_to - bpm_from) * sigmoid
            steps.append({
                "bar": i,
                "bpm": round(bpm, 2),
                "progress": round(progress, 2),
                "curve_value": round(sigmoid, 3)
            })
        return steps
    
    def _break_transition(self, bpm_from: float, bpm_to: float, 
                         bars: int) -> List[Dict[str, Any]]:
        """Transición con break en el medio"""
        steps = []
        half_bars = bars // 2
        
        # Primera mitad: desacelerar/acelerar hacia tempo intermedio
        mid_bpm = (bpm_from + bpm_to) / 2
        for i in range(half_bars + 1):
            progress = i / half_bars
            bpm = bpm_from + (mid_bpm - bpm_from) * progress
            steps.append({
                "bar": i,
                "bpm": round(bpm, 2),
                "phase": "first_half"
            })
        
        # Segunda mitad: transición al tempo final
        for i in range(1, bars - half_bars + 1):
            progress = i / (bars - half_bars)
            bpm = mid_bpm + (bpm_to - mid_bpm) * progress
            steps.append({
                "bar": half_bars + i,
                "bpm": round(bpm, 2),
                "phase": "second_half"
            })
        
        return steps
    
    def _get_transition_effects(self, diff_percent: float) -> List[str]:
        """Recomienda efectos según magnitud de cambio"""
        if diff_percent <= 3:
            return ["smooth_filter_sweep", "subtle_reverb_increase"]
        elif diff_percent <= 6:
            return ["filter_sweep", "riser", "reverb_tail", "delay_fill"]
        else:
            return ["highpass_sweep", "crash", "reverb_freeze", "delay_throw", "stutter"]


# ============================================================================
# T141: Transiciones Armónicas (Círculo de Quintas)
# ============================================================================

class HarmonicTransitionEngine:
    """T141: Motor de transiciones armónicas usando círculo de quintas"""
    
    CIRCLE_OF_FIFTHS = [
        "C", "G", "D", "A", "E", "B", "F#", "Db", "Ab", "Eb", "Bb", "F"
    ]
    
    RELATIVE_MINORS = {
        "C": "Am", "G": "Em", "D": "Bm", "A": "F#m", "E": "C#m",
        "B": "G#m", "F#": "D#m", "Db": "Bbm", "Ab": "Fm", "Eb": "Cm",
        "Bb": "Gm", "F": "Dm"
    }
    
    def get_harmonic_path(self, key_from: str, key_to: str, 
                         max_steps: int = 3) -> List[Dict[str, Any]]:
        """Encuentra camino armónico entre dos keys"""
        
        # Parsear keys (mayor/menor)
        is_from_minor = key_from.endswith('m')
        is_to_minor = key_to.endswith('m')
        
        base_from = key_from.replace('m', '').replace('M', '')
        base_to = key_to.replace('m', '').replace('M', '')
        
        # Encontrar posiciones en círculo
        try:
            idx_from = self.CIRCLE_OF_FIFTHS.index(base_from)
            idx_to = self.CIRCLE_OF_FIFTHS.index(base_to)
        except ValueError:
            return [{"error": f"Key not found in circle: {base_from} or {base_to}"}]
        
        # Calcular dirección óptima (horaria o antihoraria)
        clockwise = (idx_to - idx_from) % 12
        counter_clockwise = (idx_from - idx_to) % 12
        
        path = []
        if clockwise <= counter_clockwise:
            # Ir en sentido horario (quintas ascendentes)
            direction = "clockwise"
            for i in range(clockwise + 1):
                idx = (idx_from + i) % 12
                key = self.CIRCLE_OF_FIFTHS[idx]
                if is_from_minor and i == 0:
                    key = self.RELATIVE_MINORS.get(key, key + "m")
                elif is_to_minor and i == clockwise:
                    key = self.RELATIVE_MINORS.get(key, key + "m")
                path.append({
                    "step": i,
                    "key": key,
                    "position": idx,
                    "direction": direction,
                    "energy_change": self._get_energy_change(i, clockwise)
                })
        else:
            # Ir en sentido antihorario (quintas descendentes)
            direction = "counter_clockwise"
            for i in range(counter_clockwise + 1):
                idx = (idx_from - i) % 12
                key = self.CIRCLE_OF_FIFTHS[idx]
                if is_from_minor and i == 0:
                    key = self.RELATIVE_MINORS.get(key, key + "m")
                elif is_to_minor and i == counter_clockwise:
                    key = self.RELATIVE_MINORS.get(key, key + "m")
                path.append({
                    "step": i,
                    "key": key,
                    "position": idx,
                    "direction": direction,
                    "energy_change": self._get_energy_change(i, counter_clockwise)
                })
        
        return path
    
    def _get_energy_change(self, step: int, total_steps: int) -> str:
        """Describe cambio de energía en cada paso"""
        if step == 0:
            return "start"
        elif step == total_steps:
            return "arrival"
        elif step <= total_steps / 3:
            return "building_tension"
        elif step <= 2 * total_steps / 3:
            return "peak_transition"
        else:
            return "resolution"
    
    def get_compatible_keys(self, key: str, compatibility_threshold: float = 0.5) -> List[Dict[str, Any]]:
        """Obtiene keys compatibles ordenadas por cercanía armónica"""
        base = key.replace('m', '').replace('M', '')
        is_minor = key.endswith('m')
        
        try:
            idx = self.CIRCLE_OF_FIFTHS.index(base)
        except ValueError:
            return []
        
        compatible = []
        for i, k in enumerate(self.CIRCLE_OF_FIFTHS):
            distance = min((i - idx) % 12, (idx - i) % 12)
            
            # Score basado en distancia en círculo
            if distance == 0:
                score = 1.0  # Same key
            elif distance == 1:
                score = 0.9  # Quinta
            elif distance == 2:
                score = 0.7  # Segunda vecina
            elif distance == 3:
                score = 0.5  # Relativa lejana
            else:
                score = max(0.0, 1.0 - (distance / 6))
            
            if score >= compatibility_threshold:
                result_key = self.RELATIVE_MINORS.get(k, k) if is_minor else k
                compatible.append({
                    "key": result_key,
                    "distance": distance,
                    "compatibility_score": score,
                    "relationship": self._get_relationship_name(distance)
                })
        
        compatible.sort(key=lambda x: x['compatibility_score'], reverse=True)
        return compatible
    
    def _get_relationship_name(self, distance: int) -> str:
        """Nombre de la relación armónica"""
        names = {
            0: "same_key",
            1: "perfect_fifth",
            2: "second_neighbor",
            3: "relative_distant",
            4: "mediant",
            5: "tritone",
            6: "opposite"
        }
        return names.get(distance, "distant")


# ============================================================================
# T142: Bailout Stems Dorados
# ============================================================================

class GoldenStemsBailout:
    """T142: Sistema de stems dorados para bailout de emergencia"""
    
    def __init__(self):
        self._golden_stems_path = Path.home() / ".abletonmcp_ai" / "golden_stems.json"
        self._golden_stems: Dict[str, Any] = {}
        self._load_golden_stems()
    
    def _load_golden_stems(self):
        """Carga configuración de stems dorados"""
        try:
            if self._golden_stems_path.exists():
                with open(self._golden_stems_path, 'r', encoding='utf-8') as f:
                    self._golden_stems = json.load(f)
        except Exception as e:
            logger.warning(f"⚠ Error cargando golden stems: {e}")
    
    def register_golden_stem(self, name: str, file_path: str, 
                              bpm: float, key: str, duration_bars: int = 16,
                              category: str = "loop") -> Dict[str, Any]:
        """Registra un stem dorado para uso en bailout"""
        self._golden_stems[name] = {
            "path": file_path,
            "bpm": bpm,
            "key": key,
            "duration_bars": duration_bars,
            "category": category,
            "registered_at": time.time()
        }
        
        self._save_golden_stems()
        
        return {
            "status": "registered",
            "name": name,
            "bpm": bpm,
            "key": key
        }
    
    def get_bailout_stem(self, current_bpm: float, current_key: str,
                        category: str = "loop") -> Optional[Dict[str, Any]]:
        """Obtiene stem dorado apropiado para bailout"""
        candidates = []
        
        for name, stem in self._golden_stems.items():
            if stem.get('category') != category:
                continue
            
            # Calcular compatibilidad
            bpm_diff = abs(stem['bpm'] - current_bpm) / current_bpm
            key_match = stem['key'].lower() == current_key.lower()
            
            score = 1.0
            if bpm_diff <= 0.03:  # ±3% BPM
                score += 0.5
            elif bpm_diff <= 0.06:  # ±6% BPM
                score += 0.3
            
            if key_match:
                score += 0.5
            
            candidates.append((name, stem, score))
        
        if not candidates:
            return None
        
        # Ordenar por score y retornar mejor match
        candidates.sort(key=lambda x: x[2], reverse=True)
        best = candidates[0]
        
        return {
            "name": best[0],
            **best[1],
            "match_score": best[2]
        }
    
    def _save_golden_stems(self):
        """Guarda stems dorados a disco"""
        try:
            self._golden_stems_path.parent.mkdir(parents=True, exist_ok=True)
            with open(self._golden_stems_path, 'w', encoding='utf-8') as f:
                json.dump(self._golden_stems, f, indent=2)
        except Exception as e:
            logger.warning(f"⚠ Error guardando golden stems: {e}")


# ============================================================================
# T143: Humanize Sofisticado por Subgénero
# ============================================================================

class SubgenreHumanizer:
    """T143: Humanización avanzada adaptada por subgénero"""
    
    # Configuraciones de humanización por subgénero
    HUMANIZE_PROFILES = {
        "deep_house": {
            "timing_variation_ms": 15,
            "velocity_humanize": 0.12,
            "swing_amount": 0.08,
            "groove_template": "deep_groove",
            "random_timing": 0.05,
            "velocity_sensitivity": 0.15
        },
        "tech_house": {
            "timing_variation_ms": 8,
            "velocity_humanize": 0.08,
            "swing_amount": 0.05,
            "groove_template": "tech_tight",
            "random_timing": 0.03,
            "velocity_sensitivity": 0.10
        },
        "techno_industrial": {
            "timing_variation_ms": 5,
            "velocity_humanize": 0.15,
            "swing_amount": 0.02,
            "groove_template": "industrial_precise",
            "random_timing": 0.02,
            "velocity_sensitivity": 0.20,
            "accent_probability": 0.15
        },
        "minimal": {
            "timing_variation_ms": 3,
            "velocity_humanize": 0.05,
            "swing_amount": 0.01,
            "groove_template": "minimal_exact",
            "random_timing": 0.01,
            "velocity_sensitivity": 0.05
        },
        "latin_house": {
            "timing_variation_ms": 20,
            "velocity_humanize": 0.18,
            "swing_amount": 0.15,
            "groove_template": "latin_shuffle",
            "random_timing": 0.08,
            "velocity_sensitivity": 0.20,
            "clave_sync": True
        },
        "progressive": {
            "timing_variation_ms": 10,
            "velocity_humanize": 0.10,
            "swing_amount": 0.06,
            "groove_template": "progressive_flow",
            "random_timing": 0.04,
            "velocity_sensitivity": 0.12
        }
    }
    
    def get_humanize_profile(self, subgenre: str) -> Dict[str, Any]:
        """Obtiene perfil de humanización para subgénero"""
        profile = self.HUMANIZE_PROFILES.get(subgenre.lower(), self.HUMANIZE_PROFILES["tech_house"])
        
        return {
            "subgenre": subgenre,
            "profile": profile,
            "description": self._get_profile_description(subgenre),
            "recommended_apply": ["drums", "bass", "percussion"]
        }
    
    def _get_profile_description(self, subgenre: str) -> str:
        """Descripción del perfil de humanización"""
        descriptions = {
            "deep_house": "Groove suelto con swing moderado, timing orgánico",
            "tech_house": "Timing ajustado con mínima variación, groove preciso",
            "techno_industrial": "Precisión industrial con acentos agresivos",
            "minimal": "Timing casi perfecto, mínima humanización",
            "latin_house": "Shuffle latino con sincronización clave",
            "progressive": "Flow gradual con timing musical"
        }
        return descriptions.get(subgenre.lower(), "Perfil tech_house por defecto")
    
    def apply_humanize_by_subgenre(self, notes: List[Dict[str, Any]], 
                                  subgenre: str) -> List[Dict[str, Any]]:
        """Aplica humanización a notas según subgénero"""
        profile = self.HUMANIZE_PROFILES.get(subgenre.lower(), self.HUMANIZE_PROFILES["tech_house"])
        
        humanized = []
        for note in notes:
            # Timing variation
            timing_var = profile["timing_variation_ms"] / 1000.0  # Convertir a beats
            timing_offset = random.uniform(-timing_var, timing_var)
            
            # Velocity humanize
            vel_humanize = profile["velocity_humanize"]
            velocity_change = random.uniform(-vel_humanize, vel_humanize) * 127
            
            humanized_note = {
                **note,
                "start": note.get("start", 0) + timing_offset,
                "velocity": max(1, min(127, note.get("velocity", 100) + velocity_change)),
                "humanize_applied": True,
                "subgenre": subgenre
            }
            humanized.append(humanized_note)
        
        return humanized


# ============================================================================
# T144: Análisis de Fatiga Temporal
# ============================================================================

class TemporalFatigueAnalyzer:
    """T144: Analiza fatiga de samples con sistema de descanso"""
    
    def __init__(self):
        self._fatigue_path = Path.home() / ".abletonmcp_ai" / "temporal_fatigue.json"
        self._usage_history: Dict[str, List[float]] = {}  # sample_path -> [timestamps]
        self._fatigue_scores: Dict[str, float] = {}  # sample_path -> fatigue_score
        self._load_data()
    
    def _load_data(self):
        """Carga datos de fatiga"""
        try:
            if self._fatigue_path.exists():
                with open(self._fatigue_path, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    self._usage_history = data.get('history', {})
                    self._fatigue_scores = data.get('fatigue', {})
        except Exception as e:
            logger.warning(f"⚠ Error cargando fatiga temporal: {e}")
    
    def record_usage(self, sample_path: str):
        """Registra uso de un sample"""
        if sample_path not in self._usage_history:
            self._usage_history[sample_path] = []
        
        self._usage_history[sample_path].append(time.time())
        
        # Recalcular fatiga
        self._update_fatigue_score(sample_path)
        
        self._save_data()
    
    def _update_fatigue_score(self, sample_path: str):
        """Actualiza score de fatiga basado en uso reciente"""
        usages = self._usage_history.get(sample_path, [])
        now = time.time()
        
        # Calcular fatiga basada en usos en últimas 24 horas
        recent_usages = [u for u in usages if now - u < 86400]  # 24 horas
        
        if len(recent_usages) == 0:
            fatigue = 0.0
        elif len(recent_usages) <= 2:
            fatigue = 0.2
        elif len(recent_usages) <= 5:
            fatigue = 0.5
        elif len(recent_usages) <= 10:
            fatigue = 0.8
        else:
            fatigue = 1.0
        
        # Reducir fatiga basado en tiempo desde último uso
        if usages:
            hours_since_last = (now - usages[-1]) / 3600
            recovery = min(1.0, hours_since_last / 24)  # Recuperación completa en 24h
            fatigue *= (1.0 - recovery)
        
        self._fatigue_scores[sample_path] = fatigue
    
    def get_fatigue_report(self, sample_path: Optional[str] = None) -> Dict[str, Any]:
        """Obtiene reporte de fatiga"""
        if sample_path:
            fatigue = self._fatigue_scores.get(sample_path, 0.0)
            history = self._usage_history.get(sample_path, [])
            
            return {
                "sample": sample_path,
                "fatigue_score": fatigue,
                "total_uses": len(history),
                "recent_uses_24h": len([h for h in history if time.time() - h < 86400]),
                "recommendation": self._get_fatigue_recommendation(fatigue),
                "rest_time_remaining_ms": self._calculate_rest_time(fatigue)
            }
        
        # Reporte global
        return {
            "samples_tracked": len(self._fatigue_scores),
            "average_fatigue": sum(self._fatigue_scores.values()) / max(1, len(self._fatigue_scores)),
            "most_fatigued": sorted(self._fatigue_scores.items(), key=lambda x: x[1], reverse=True)[:10],
            "least_fatigued": sorted(self._fatigue_scores.items(), key=lambda x: x[1])[:10]
        }
    
    def _get_fatigue_recommendation(self, fatigue: float) -> str:
        """Genera recomendación basada en fatiga"""
        if fatigue < 0.2:
            return "Sample fresh - safe to use"
        elif fatigue < 0.5:
            return "Moderate use - consider variety"
        elif fatigue < 0.8:
            return "High fatigue - recommend rest (6+ hours)"
        else:
            return "Critical fatigue - mandatory rest (24+ hours)"
    
    def _calculate_rest_time(self, fatigue: float) -> int:
        """Calcula tiempo de descanso recomendado en ms"""
        if fatigue < 0.2:
            return 0
        elif fatigue < 0.5:
            return 21600000  # 6 horas
        elif fatigue < 0.8:
            return 43200000  # 12 horas
        else:
            return 86400000  # 24 horas
    
    def _save_data(self):
        """Guarda datos de fatiga"""
        try:
            self._fatigue_path.parent.mkdir(parents=True, exist_ok=True)
            with open(self._fatigue_path, 'w', encoding='utf-8') as f:
                json.dump({
                    'history': self._usage_history,
                    'fatigue': self._fatigue_scores
                }, f, indent=2)
        except Exception as e:
            logger.warning(f"⚠ Error guardando fatiga temporal: {e}")


# ============================================================================
# T145: Monitor de Latencia MCP
# ============================================================================

class MCPLatencyMonitor:
    """T145: Monitorea latencia del servidor MCP para detectar hangs"""
    
    def __init__(self, threshold_ms: float = 5000.0):
        self.threshold_ms = threshold_ms
        self._response_times: deque = deque(maxlen=100)
        self._hang_count = 0
        self._last_check = time.time()
        self._is_healthy = True
    
    def record_response_time(self, duration_ms: float):
        """Registra tiempo de respuesta"""
        self._response_times.append({
            "timestamp": time.time(),
            "duration_ms": duration_ms
        })
        
        # Detectar hang
        if duration_ms > self.threshold_ms:
            self._hang_count += 1
            self._is_healthy = False
            logger.warning(f"⚠ T145: MCP Hang detectado: {duration_ms:.0f}ms")
        else:
            self._is_healthy = True
    
    def get_health_status(self) -> Dict[str, Any]:
        """Obtiene estado de salud del servidor"""
        if not self._response_times:
            return {"status": "unknown", "message": "No data yet"}
        
        recent = [r for r in self._response_times 
                 if time.time() - r["timestamp"] < 300]  # Últimos 5 min
        
        if not recent:
            return {"status": "warning", "message": "No recent data"}
        
        avg_time = sum(r["duration_ms"] for r in recent) / len(recent)
        max_time = max(r["duration_ms"] for r in recent)
        min_time = min(r["duration_ms"] for r in recent)
        
        return {
            "status": "healthy" if self._is_healthy else "degraded",
            "average_response_ms": round(avg_time, 2),
            "max_response_ms": round(max_time, 2),
            "min_response_ms": round(min_time, 2),
            "hangs_detected": self._hang_count,
            "samples_in_avg": len(recent),
            "threshold_ms": self.threshold_ms,
            "recommendation": self._get_health_recommendation(avg_time)
        }
    
    def _get_health_recommendation(self, avg_time: float) -> str:
        """Genera recomendación de salud"""
        if avg_time < 100:
            return "Excellent response times"
        elif avg_time < 500:
            return "Good performance"
        elif avg_time < 1000:
            return "Acceptable but monitor"
        elif avg_time < self.threshold_ms:
            return "Slow - consider optimization"
        else:
            return "Critical - restart recommended"


# ============================================================================
# T146: Exportador CUE Points Dinámicos
# ============================================================================

class CUEPointExporter:
    """T146: Exporta CUE points dinámicos para DJs"""
    
    def __init__(self):
        self._cue_formats = {
            "rekordbox": self._export_rekordbox,
            "serato": self._export_serato,
            "traktor": self._export_traktor,
            "mixed_in_key": self._export_mixed_in_key,
            "ableton": self._export_ableton
        }
    
    def generate_cue_points(self, sections: List[Dict[str, Any]], 
                           track_info: Dict[str, Any]) -> List[Dict[str, Any]]:
        """Genera CUE points basados en secciones del track"""
        cues = []
        
        # CUE point types y colores
        cue_types = {
            "intro": {"type": 0, "color": "green"},
            "build": {"type": 1, "color": "orange"},
            "drop": {"type": 2, "color": "red"},
            "break": {"type": 3, "color": "blue"},
            "outro": {"type": 4, "color": "purple"},
            "verse": {"type": 5, "color": "yellow"},
            "chorus": {"type": 6, "color": "cyan"}
        }
        
        for i, section in enumerate(sections):
            section_type = section.get("type", "unknown").lower()
            position_ms = int(section.get("start_bar", 0) * 4 * (60000 / track_info.get("bpm", 128)))
            
            cue_info = cue_types.get(section_type, {"type": 7, "color": "white"})
            
            cues.append({
                "index": i,
                "position_ms": position_ms,
                "position_bar": section.get("start_bar", 0),
                "type": cue_info["type"],
                "color": cue_info["color"],
                "name": f"{section_type.upper()}",
                "bpm": track_info.get("bpm", 128),
                "key": track_info.get("key", "Unknown")
            })
        
        return cues
    
    def export_cues(self, cues: List[Dict[str, Any]], 
                   format: str, output_path: str) -> Dict[str, Any]:
        """Exporta CUE points a formato específico"""
        if format not in self._cue_formats:
            return {"error": f"Format {format} not supported"}
        
        try:
            result = self._cue_formats[format](cues, output_path)
            return {
                "status": "success",
                "format": format,
                "output_path": output_path,
                "cue_count": len(cues),
                "result": result
            }
        except Exception as e:
            return {"error": str(e)}
    
    def _export_rekordbox(self, cues: List[Dict[str, Any]], path: str) -> str:
        """Exporta formato Rekordbox (Pioneer)"""
        # Formato XML simplificado
        xml_content = '<?xml version="1.0" encoding="UTF-8"?>\n'
        xml_content += '<DJ_PLAYLISTS Version="1.0.0">\n'
        xml_content += '  <PRODUCT Name="rekordbox" Version="6.0"/>\n'
        xml_content += '  <TRACK>\n'
        
        for cue in cues:
            xml_content += f'    <POSITION_MARK Type="{cue["type"]}" "'
            xml_content += f'Name="{cue["name"]}" "'
            xml_content += f'Start="{cue["position_ms"]}"/>\n'
        
        xml_content += '  </TRACK>\n'
        xml_content += '</DJ_PLAYLISTS>\n'
        
        Path(path).write_text(xml_content, encoding='utf-8')
        return f"Rekordbox XML exported: {len(cues)} cues"
    
    def _export_serato(self, cues: List[Dict[str, Any]], path: str) -> str:
        """Exporta formato Serato"""
        lines = []
        for cue in cues:
            lines.append(f"{cue['index']}|{cue['position_ms']}|{cue['name']}|{cue['color']}")
        
        Path(path).write_text('\n'.join(lines), encoding='utf-8')
        return f"Serato markers exported: {len(cues)} cues"
    
    def _export_traktor(self, cues: List[Dict[str, Any]], path: str) -> str:
        """Exporta formato Traktor (NI)"""
        entries = []
        for cue in cues:
            entries.append({
                "INDEX": cue["index"],
                "POSITION": cue["position_ms"],
                "NAME": cue["name"],
                "TYPE": cue["type"]
            })
        
        Path(path).write_text(json.dumps(entries, indent=2), encoding='utf-8')
        return f"Traktor JSON exported: {len(cues)} cues"
    
    def _export_mixed_in_key(self, cues: List[Dict[str, Any]], path: str) -> str:
        """Exporta formato Mixed In Key"""
        lines = [f"BPM: {cues[0].get('bpm', 128) if cues else 128}"]
        lines.append(f"KEY: {cues[0].get('key', 'Unknown') if cues else 'Unknown'}")
        for cue in cues:
            lines.append(f"{cue['position_ms']} - {cue['name']} ({cue['color']})")
        
        Path(path).write_text('\n'.join(lines), encoding='utf-8')
        return f"MIK text exported: {len(cues)} cues"
    
    def _export_ableton(self, cues: List[Dict[str, Any]], path: str) -> str:
        """Exporta formato Ableton Live locators"""
        # Formato JSON para importación a Live
        locators = []
        for cue in cues:
            locators.append({
                "time": cue["position_bar"],
                "name": cue["name"],
                "color": cue["color"]
            })
        
        Path(path).write_text(json.dumps(locators, indent=2), encoding='utf-8')
        return f"Ableton locators exported: {len(cues)} cues"


# ============================================================================
# T147: Análisis de Tendencias de Librería
# ============================================================================

class LibraryTrendsAnalyzer:
    """T147: Analiza tendencias de BPM y Key en la librería"""
    
    def __init__(self, sample_manager=None):
        self.sample_manager = sample_manager
    
    def analyze_trends(self, samples: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
        """Analiza tendencias de la librería de samples"""
        if samples is None:
            samples = self._get_all_samples()
        
        # Análisis BPM
        bpm_distribution = self._analyze_bpm_distribution(samples)
        
        # Análisis Key
        key_distribution = self._analyze_key_distribution(samples)
        
        # Análisis de géneros
        genre_distribution = self._analyze_genre_distribution(samples)
        
        # BPMs y Keys predominantes
        dominant_bpms = self._get_dominant_values(bpm_distribution, top_n=5)
        dominant_keys = self._get_dominant_values(key_distribution, top_n=5)
        
        # Insights
        insights = self._generate_insights(bpm_distribution, key_distribution, genre_distribution)
        
        return {
            "status": "success",
            "samples_analyzed": len(samples),
            "dominant_bpms": dominant_bpms,
            "dominant_keys": dominant_keys,
            "dominant_genres": self._get_dominant_values(genre_distribution, top_n=5),
            "bpm_distribution": bpm_distribution,
            "key_distribution": key_distribution,
            "genre_distribution": genre_distribution,
            "insights": insights,
            "recommendations": self._generate_recommendations(insights)
        }
    
    def _get_all_samples(self) -> List[Dict[str, Any]]:
        """Obtiene todos los samples disponibles"""
        # Placeholder - implementar con sample_manager real
        return []
    
    def _analyze_bpm_distribution(self, samples: List[Dict[str, Any]]) -> Dict[str, int]:
        """Analiza distribución de BPM"""
        distribution = defaultdict(int)
        
        for sample in samples:
            bpm = sample.get('bpm', 0)
            if bpm > 0:
                # Agrupar en rangos
                if bpm < 100:
                    bucket = "slow (<100)"
                elif bpm < 120:
                    bucket = "mid-slow (100-120)"
                elif bpm < 130:
                    bucket = "mid (120-130)"
                elif bpm < 140:
                    bucket = "fast (130-140)"
                else:
                    bucket = "very fast (140+)"
                
                distribution[bucket] += 1
                
                # También por BPM exacto (agrupado)
                rounded_bpm = round(bpm / 5) * 5
                distribution[f"{rounded_bpm}"] += 1
        
        return dict(distribution)
    
    def _analyze_key_distribution(self, samples: List[Dict[str, Any]]) -> Dict[str, int]:
        """Analiza distribución de Keys"""
        distribution = defaultdict(int)
        
        for sample in samples:
            key = sample.get('key', '')
            if key:
                distribution[key] += 1
                
                # Agrupar por tonalidad (mayor/menor)
                if 'm' in key.lower():
                    distribution["minor_keys"] += 1
                else:
                    distribution["major_keys"] += 1
        
        return dict(distribution)
    
    def _analyze_genre_distribution(self, samples: List[Dict[str, Any]]) -> Dict[str, int]:
        """Analiza distribución de géneros"""
        distribution = defaultdict(int)
        
        for sample in samples:
            genre = sample.get('genre', 'unknown')
            if isinstance(genre, str):
                distribution[genre] += 1
            elif isinstance(genre, list):
                for g in genre:
                    distribution[g] += 1
        
        return dict(distribution)
    
    def _get_dominant_values(self, distribution: Dict[str, int], top_n: int = 5) -> List[Dict[str, Any]]:
        """Obtiene valores dominantes de una distribución"""
        sorted_items = sorted(distribution.items(), key=lambda x: x[1], reverse=True)
        
        result = []
        for key, count in sorted_items[:top_n]:
            percentage = count / sum(distribution.values()) * 100 if distribution else 0
            result.append({
                "value": key,
                "count": count,
                "percentage": round(percentage, 2)
            })
        
        return result
    
    def _generate_insights(self, bpm_dist: Dict, key_dist: Dict, genre_dist: Dict) -> List[str]:
        """Genera insights basados en distribuciones"""
        insights = []
        
        # Insights BPM
        if bpm_dist:
            top_bpm = max(bpm_dist.items(), key=lambda x: x[1])
            insights.append(f"BPM sweet spot: {top_bpm[0]} ({top_bpm[1]} samples)")
        
        # Insights Key
        if key_dist:
            minor_count = key_dist.get('minor_keys', 0)
            major_count = key_dist.get('major_keys', 0)
            total = minor_count + major_count
            if total > 0:
                if minor_count / total > 0.6:
                    insights.append("Library is predominantly minor key (good for techno/house)")
                elif major_count / total > 0.6:
                    insights.append("Library is predominantly major key (good for uplifting/happy)")
        
        # Insights género
        if genre_dist:
            top_genre = max(genre_dist.items(), key=lambda x: x[1])
            insights.append(f"Dominant genre: {top_genre[0]} ({top_genre[1]} samples)")
        
        return insights
    
    def _generate_recommendations(self, insights: List[str]) -> List[str]:
        """Genera recomendaciones basadas en insights"""
        recommendations = [
            "Consider generating tracks in dominant BPM range for better sample utilization",
            "Explore keys that are harmonically compatible with dominant keys",
            "Check if genre distribution matches your production goals"
        ]
        return recommendations


# ============================================================================
# T148: Algoritmo Predictivo de Track (Entropía de Energía)
# ============================================================================

class PredictiveTrackAlgorithm:
    """T148: Algoritmo predictivo para siguiente track basado en entropía de energía"""
    
    def __init__(self):
        self._energy_history: deque = deque(maxlen=50)
        self._transition_patterns: List[Dict[str, Any]] = []
    
    def record_energy_state(self, section: str, energy: float, bpm: float, key: str):
        """Registra estado de energía actual"""
        self._energy_history.append({
            "timestamp": time.time(),
            "section": section,
            "energy": energy,
            "bpm": bpm,
            "key": key
        })
    
    def predict_next_track(self, available_tracks: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Predice el siguiente track óptimo"""
        if not self._energy_history:
            return {"error": "No energy history available"}
        
        current = self._energy_history[-1]
        current_energy = current["energy"]
        current_bpm = current["bpm"]
        current_key = current["key"]
        
        # Calcular entropía de energía reciente
        entropy = self._calculate_energy_entropy()
        
        # Calcular scores para cada track disponible
        scored_tracks = []
        for track in available_tracks:
            score = 0.0
            factors = []
            
            # Factor 1: Continuidad energética
            track_energy = track.get('energy_profile', [0.5])[0]
            energy_diff = abs(track_energy - current_energy)
            energy_score = max(0, 1.0 - energy_diff)
            score += energy_score * 30
            factors.append(f"energy_continuity: {energy_score:.2f}")
            
            # Factor 2: Compatibilidad BPM
            track_bpm = track.get('bpm', current_bpm)
            bpm_diff = abs(track_bpm - current_bpm) / current_bpm
            if bpm_diff <= 0.03:
                bpm_score = 1.0
            elif bpm_diff <= 0.06:
                bpm_score = 0.7
            else:
                bpm_score = max(0, 1.0 - bpm_diff * 2)
            score += bpm_score * 25
            factors.append(f"bpm_compat: {bpm_score:.2f}")
            
            # Factor 3: Compatibilidad armónica
            track_key = track.get('key', current_key)
            key_compat = self._calculate_key_compatibility(current_key, track_key)
            score += key_compat * 25
            factors.append(f"key_compat: {key_compat:.2f}")
            
            # Factor 4: Diversidad (basado en entropía)
            # Si entropía es baja, favorecer cambios; si es alta, favorecer continuidad
            diversity_score = self._calculate_diversity_score(track, entropy)
            score += diversity_score * 20
            factors.append(f"diversity: {diversity_score:.2f}")
            
            scored_tracks.append({
                **track,
                "predicted_score": score,
                "score_factors": factors
            })
        
        # Ordenar por score
        scored_tracks.sort(key=lambda x: x['predicted_score'], reverse=True)
        
        return {
            "current_energy": current_energy,
            "energy_entropy": entropy,
            "current_bpm": current_bpm,
            "current_key": current_key,
            "recommendations": scored_tracks[:5],
            "top_recommendation": scored_tracks[0] if scored_tracks else None,
            "algorithm": "energy_entropy_based"
        }
    
    def _calculate_energy_entropy(self) -> float:
        """Calcula entropía de Shannon de la distribución de energía"""
        if len(self._energy_history) < 2:
            return 0.5
        
        # Discretizar niveles de energía
        energy_bins = defaultdict(int)
        for state in self._energy_history:
            # Bin de 0.0 a 1.0 en pasos de 0.2
            bin_idx = min(4, int(state["energy"] / 0.2))
            energy_bins[bin_idx] += 1
        
        # Calcular entropía
        total = len(self._energy_history)
        entropy = 0.0
        for count in energy_bins.values():
            p = count / total
            if p > 0:
                entropy -= p * (p).bit_length()  # Approximación log2
        
        # Normalizar a 0-1
        max_entropy = 2.32  # log2(5) para 5 bins
        return min(1.0, entropy / max_entropy) if max_entropy > 0 else 0.5
    
    def _calculate_key_compatibility(self, key1: str, key2: str) -> float:
        """Calcula compatibilidad entre keys (0-1)"""
        # Implementación simplificada - usar HarmonicTransitionEngine para más precisión
        if key1.lower() == key2.lower():
            return 1.0
        
        base1 = key1.replace('m', '').replace('M', '')
        base2 = key2.replace('m', '').replace('M', '')
        
        if base1 == base2:
            return 0.9  # Misma tonalidad, diferente modo
        
        # Círculo de quintas simplificado
        circle = ["C", "G", "D", "A", "E", "B", "F#", "Db", "Ab", "Eb", "Bb", "F"]
        try:
            idx1 = circle.index(base1)
            idx2 = circle.index(base2)
            distance = min((idx2 - idx1) % 12, (idx1 - idx2) % 12)
            return max(0.0, 1.0 - distance * 0.15)
        except ValueError:
            return 0.5
    
    def _calculate_diversity_score(self, track: Dict[str, Any], entropy: float) -> float:
        """Calcula score de diversidad basado en entropía actual"""
        # Si entropía es baja, queremos más diversidad
        # Si entropía es alta, queremos más estabilidad
        target_diversity = 1.0 - entropy
        
        # Calcular diversidad del track
        track_genre = track.get('genre', 'unknown')
        track_energy_variance = track.get('energy_variance', 0.5)
        
        # Score más alto cuando track aporta diversidad necesaria
        diversity_match = 1.0 - abs(track_energy_variance - target_diversity)
        
        return diversity_match


# ============================================================================
# T149: Colores Semánticos por Role
# ============================================================================

class SemanticColorMapper:
    """T149: Mapeo semántico de colores por rol de track"""
    
    # Mapeo de roles a colores Ableton (0-69)
    ROLE_COLORS = {
        # Drums - Rojos/Naranjas (0-19)
        "kick": 6,        # Rojo brillante
        "snare": 8,       # Rojo naranja
        "clap": 10,       # Naranja
        "hat": 13,        # Naranja amarillo
        "hat_closed": 13,
        "hat_open": 14,
        "perc": 16,       # Amarillo
        "perc_loop": 16,
        "top_loop": 18,
        "drums": 5,       # Rojo general
        
        # Bass - Amarillos/Verdes (20-29)
        "bass": 25,       # Amarillo verdoso
        "bass_loop": 25,
        "sub_bass": 27,   # Verde lima
        
        # Harmónicos/Music - Verdes/Azules (30-49)
        "synth_loop": 35, # Verde azulado
        "synth_peak": 37, # Turquesa
        "pad": 40,        # Azul claro
        "chord": 42,      # Azul
        "lead": 45,       # Azul púrpura
        "arp": 48,        # Púrpura
        "music": 35,      # Verde azulado general
        
        # Vocals - Púrpuras/Rosas (50-59)
        "vocal": 52,      # Púrpura rosa
        "vocal_loop": 52,
        "vocal_shot": 54, # Rosa
        "vocal_build": 55,
        "vocal_peak": 56,
        
        # FX - Grises/Especiales (60-69)
        "atmos_fx": 60,   # Gris claro
        "crash_fx": 62,   # Gris
        "riser": 64,      # Gris oscuro
        "fill_fx": 66,    # Blanco
        "snare_roll": 68, # Negro
        "drone": 65,
        "fx": 60,
        
        # Default
        "default": 0
    }
    
    def get_color_for_role(self, role: str) -> int:
        """Obtiene color semántico para un rol"""
        role_lower = role.lower().strip()
        
        # Buscar match exacto
        if role_lower in self.ROLE_COLORS:
            return self.ROLE_COLORS[role_lower]
        
        # Buscar match parcial
        for role_key, color in self.ROLE_COLORS.items():
            if role_key in role_lower or role_lower in role_key:
                return color
        
        return self.ROLE_COLORS["default"]
    
    def get_color_info(self, role: str) -> Dict[str, Any]:
        """Obtiene información completa del color para un rol"""
        color = self.get_color_for_role(role)
        
        color_names = {
            0: "Default/Red", 5: "Drums Red", 6: "Kick Red", 8: "Snare Red",
            10: "Clap Orange", 13: "Hat Yellow-Orange", 16: "Perc Yellow",
            18: "Top Yellow", 25: "Bass Yellow-Green", 27: "Sub Green",
            35: "Synth Teal", 37: "Peak Turquoise", 40: "Pad Light Blue",
            42: "Chord Blue", 45: "Lead Purple-Blue", 48: "Arp Purple",
            52: "Vocal Pink-Purple", 54: "Vocal Shot Pink", 
            60: "Atmos Grey", 62: "Crash Grey", 64: "Riser Dark Grey",
            66: "Fill White", 68: "Snare Roll Black"
        }
        
        return {
            "role": role,
            "color_index": color,
            "color_name": color_names.get(color, "Custom"),
            "category": self._get_color_category(color)
        }
    
    def _get_color_category(self, color: int) -> str:
        """Obtiene categoría de color"""
        if 0 <= color <= 19:
            return "reds_oranges"
        elif 20 <= color <= 29:
            return "yellows_greens"
        elif 30 <= color <= 49:
            return "blues_purples"
        elif 50 <= color <= 59:
            return "purples_pinks"
        else:
            return "greys_specials"
    
    def apply_semantic_colors(self, tracks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Aplica colores semánticos a una lista de tracks"""
        colored_tracks = []
        for track in tracks:
            role = track.get('role', 'default')
            color_info = self.get_color_info(role)
            
            colored_tracks.append({
                **track,
                "semantic_color": color_info["color_index"],
                "color_info": color_info
            })
        
        return colored_tracks


# ============================================================================
# T150: Nomenclatura MIDI Tracks
# ============================================================================

class MIDITrackNomenclature:
    """T150: Sistema de nomenclatura para tracks MIDI"""
    
    def generate_track_name(self, track_type: str, bpm: float, key: str,
                           additional_info: Optional[str] = None) -> str:
        """Genera nombre estándar para track MIDI"""
        # Formato: [MIDI] Type - BPM - Key [Additional]
        base = f"[MIDI] {track_type} - {int(bpm)} BPM - {key}"
        
        if additional_info:
            base += f" [{additional_info}]"
        
        return base
    
    def parse_track_name(self, name: str) -> Dict[str, Any]:
        """Parsea un nombre de track MIDI"""
        result = {
            "is_midi_track": False,
            "track_type": None,
            "bpm": None,
            "key": None,
            "additional_info": None
        }
        
        if not name.startswith("[MIDI]"):
            return result
        
        result["is_midi_track"] = True
        
        # Parsear componentes
        try:
            # Formato: [MIDI] Type - BPM - Key [Additional]
            parts = name.replace("[MIDI] ", "").split(" - ")
            
            if len(parts) >= 1:
                result["track_type"] = parts[0].strip()
            
            if len(parts) >= 2:
                bpm_part = parts[1].strip()
                if "BPM" in bpm_part:
                    result["bpm"] = int(bpm_part.replace("BPM", "").strip())
            
            if len(parts) >= 3:
                key_part = parts[2].strip()
                # Extraer key y additional info si existe
                if "[" in key_part:
                    key_end = key_part.find("[")
                    result["key"] = key_part[:key_end].strip()
                    result["additional_info"] = key_part[key_end:].strip("[]")
                else:
                    result["key"] = key_part
        
        except Exception as e:
            result["parse_error"] = str(e)
        
        return result
    
    def generate_standard_names(self, bpm: float, key: str) -> Dict[str, str]:
        """Genera nombres estándar para tipos comunes de tracks MIDI"""
        return {
            "arp": self.generate_track_name("Arp", bpm, key),
            "bass": self.generate_track_name("Bass", bpm, key),
            "chords": self.generate_track_name("Chords", bpm, key),
            "lead": self.generate_track_name("Lead", bpm, key),
            "pad": self.generate_track_name("Pad", bpm, key),
            "pluck": self.generate_track_name("Pluck", bpm, key),
            "stab": self.generate_track_name("Stab", bpm, key),
            "drums": self.generate_track_name("Drums", bpm, key, "Pattern"),
            "percussion": self.generate_track_name("Perc", bpm, key, "Groove")
        }


# ============================================================================
# Factory Functions
# ============================================================================

# Instancias singleton
_searcher: Optional[AdvancedSampleSearcher] = None
_palette_lock: Optional[PersistentPaletteLock] = None
_miniset_chainer: Optional[MiniSetChainer] = None
_dj_transition: Optional[DJTransitionEngine] = None
_harmonic_transition: Optional[HarmonicTransitionEngine] = None
_golden_stems: Optional[GoldenStemsBailout] = None
_subgenre_humanizer: Optional[SubgenreHumanizer] = None
_fatigue_analyzer: Optional[TemporalFatigueAnalyzer] = None
_latency_monitor: Optional[MCPLatencyMonitor] = None
_cue_exporter: Optional[CUEPointExporter] = None
_trends_analyzer: Optional[LibraryTrendsAnalyzer] = None
_predictive_algorithm: Optional[PredictiveTrackAlgorithm] = None
_semantic_colors: Optional[SemanticColorMapper] = None
_midi_nomenclature: Optional[MIDITrackNomenclature] = None


def get_advanced_searcher() -> AdvancedSampleSearcher:
    global _searcher
    if _searcher is None:
        _searcher = AdvancedSampleSearcher()
    return _searcher


def get_persistent_palette_lock() -> PersistentPaletteLock:
    global _palette_lock
    if _palette_lock is None:
        _palette_lock = PersistentPaletteLock()
    return _palette_lock


def get_miniset_chainer() -> MiniSetChainer:
    global _miniset_chainer
    if _miniset_chainer is None:
        _miniset_chainer = MiniSetChainer()
    return _miniset_chainer


def get_dj_transition_engine() -> DJTransitionEngine:
    global _dj_transition
    if _dj_transition is None:
        _dj_transition = DJTransitionEngine()
    return _dj_transition


def get_harmonic_transition_engine() -> HarmonicTransitionEngine:
    global _harmonic_transition
    if _harmonic_transition is None:
        _harmonic_transition = HarmonicTransitionEngine()
    return _harmonic_transition


def get_golden_stems_bailout() -> GoldenStemsBailout:
    global _golden_stems
    if _golden_stems is None:
        _golden_stems = GoldenStemsBailout()
    return _golden_stems


def get_subgenre_humanizer() -> SubgenreHumanizer:
    global _subgenre_humanizer
    if _subgenre_humanizer is None:
        _subgenre_humanizer = SubgenreHumanizer()
    return _subgenre_humanizer


def get_fatigue_analyzer() -> TemporalFatigueAnalyzer:
    global _fatigue_analyzer
    if _fatigue_analyzer is None:
        _fatigue_analyzer = TemporalFatigueAnalyzer()
    return _fatigue_analyzer


def get_latency_monitor() -> MCPLatencyMonitor:
    global _latency_monitor
    if _latency_monitor is None:
        _latency_monitor = MCPLatencyMonitor()
    return _latency_monitor


def get_cue_exporter() -> CUEPointExporter:
    global _cue_exporter
    if _cue_exporter is None:
        _cue_exporter = CUEPointExporter()
    return _cue_exporter


def get_trends_analyzer() -> LibraryTrendsAnalyzer:
    global _trends_analyzer
    if _trends_analyzer is None:
        _trends_analyzer = LibraryTrendsAnalyzer()
    return _trends_analyzer


def get_predictive_algorithm() -> PredictiveTrackAlgorithm:
    global _predictive_algorithm
    if _predictive_algorithm is None:
        _predictive_algorithm = PredictiveTrackAlgorithm()
    return _predictive_algorithm


def get_semantic_color_mapper() -> SemanticColorMapper:
    global _semantic_colors
    if _semantic_colors is None:
        _semantic_colors = SemanticColorMapper()
    return _semantic_colors


def get_midi_nomenclature() -> MIDITrackNomenclature:
    global _midi_nomenclature
    if _midi_nomenclature is None:
        _midi_nomenclature = MIDITrackNomenclature()
    return _midi_nomenclature
