"""
Reference Matcher - Analyzes reference tracks and creates user sound profiles.

Este módulo analiza archivos de referencia (como reggaeton_ejemplo.mp3),
extrae sus características espectrales y genera un perfil de sonido
personalizado para el usuario basado en samples similares de la librería.
"""
import json
import logging
import os
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, field, asdict
from collections import Counter

# Conditional numpy import - gracefully degrades when unavailable
try:
    import numpy as np
    NUMPY_AVAILABLE = True
except ImportError:
    NUMPY_AVAILABLE = False
    np = None  # type: ignore

logger = logging.getLogger("ReferenceMatcher")

# Paths
LIBRERIA_DIR = Path(r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\libreria")
REGGAETON_DIR = LIBRERIA_DIR / "reggaeton"
REFERENCE_FILE = LIBRERIA_DIR / "reggaeton_ejemplo.mp3"
PROFILE_FILE = REGGAETON_DIR / ".user_sound_profile.json"

# Roles de samples soportados
SAMPLE_ROLES = ["kick", "snare", "clap", "hat_closed", "hat_open", 
                "bass", "synth", "fx", "perc", "drum_loop"]


@dataclass
class SpectralFingerprint:
    """Fingerprint espectral completo de un audio."""
    bpm: float = 0.0
    key: str = ""
    energy_curve: List[float] = field(default_factory=list)
    mfccs_mean: List[float] = field(default_factory=list)
    spectral_centroid_mean: float = 0.0
    onset_strength_mean: float = 0.0
    duration: float = 0.0
    sample_rate: int = 0

    def to_dict(self) -> Dict[str, Any]:
        return {
            "bpm": self.bpm,
            "key": self.key,
            "energy_curve": self.energy_curve,
            "mfccs_mean": self.mfccs_mean,
            "spectral_centroid_mean": self.spectral_centroid_mean,
            "onset_strength_mean": self.onset_strength_mean,
            "duration": self.duration,
            "sample_rate": self.sample_rate
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "SpectralFingerprint":
        return cls(
            bpm=data.get("bpm", 0.0),
            key=data.get("key", ""),
            energy_curve=data.get("energy_curve", []),
            mfccs_mean=data.get("mfccs_mean", []),
            spectral_centroid_mean=data.get("spectral_centroid_mean", 0.0),
            onset_strength_mean=data.get("onset_strength_mean", 0.0),
            duration=data.get("duration", 0.0),
            sample_rate=data.get("sample_rate", 0)
        )


@dataclass
class SampleMatch:
    """Resultado de comparación de un sample contra referencia."""
    path: str
    name: str
    role: str
    similarity_score: float
    fingerprint: SpectralFingerprint


@dataclass
class UserSoundProfile:
    """Perfil de sonido personalizado del usuario."""
    # Características promedio ponderadas
    preferred_bpm: float = 0.0
    preferred_key: str = ""
    preferred_timbre: List[float] = field(default_factory=list)
    characteristic_energy_curve: List[float] = field(default_factory=list)
    
    # Roles más usados (ordenados por frecuencia)
    preferred_roles: List[str] = field(default_factory=list)
    
    # Metadata
    created_from_reference: str = ""
    total_matches_analyzed: int = 0
    genre: str = "reggaeton"
    
    # Matches más similares por rol
    top_matches_by_role: Dict[str, List[Dict]] = field(default_factory=dict)
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "preferred_bpm": self.preferred_bpm,
            "preferred_key": self.preferred_key,
            "preferred_timbre": self.preferred_timbre,
            "characteristic_energy_curve": self.characteristic_energy_curve,
            "preferred_roles": self.preferred_roles,
            "created_from_reference": self.created_from_reference,
            "total_matches_analyzed": self.total_matches_analyzed,
            "genre": self.genre,
            "top_matches_by_role": self.top_matches_by_role
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "UserSoundProfile":
        return cls(
            preferred_bpm=data.get("preferred_bpm", 0.0),
            preferred_key=data.get("preferred_key", ""),
            preferred_timbre=data.get("preferred_timbre", []),
            characteristic_energy_curve=data.get("characteristic_energy_curve", []),
            preferred_roles=data.get("preferred_roles", []),
            created_from_reference=data.get("created_from_reference", ""),
            total_matches_analyzed=data.get("total_matches_analyzed", 0),
            genre=data.get("genre", "reggaeton"),
            top_matches_by_role=data.get("top_matches_by_role", {})
        )


class AudioAnalyzer:
    """Analiza archivos de audio y extrae fingerprints espectrales."""
    
    def __init__(self):
        self._librosa_available = self._check_librosa()
    
    def _check_librosa(self) -> bool:
        """Verifica si librosa está disponible."""
        try:
            import librosa
            import librosa.display
            return True
        except ImportError:
            logger.warning("librosa no disponible. Usando modo simulado.")
            return False
    
    def analyze_file(self, file_path: str) -> Optional[SpectralFingerprint]:
        """
        Analiza un archivo de audio y extrae su fingerprint espectral.
        
        Args:
            file_path: Ruta al archivo de audio
            
        Returns:
            SpectralFingerprint con todas las características extraídas
        """
        if not os.path.exists(file_path):
            logger.error("Archivo no encontrado: %s", file_path)
            return None
        
        if self._librosa_available:
            return self._analyze_with_librosa(file_path)
        else:
            return self._generate_mock_fingerprint(file_path)
    
    def _analyze_with_librosa(self, file_path: str) -> Optional[SpectralFingerprint]:
        """Análisis real usando librosa."""
        try:
            import librosa
            import librosa.display
            
            # Cargar audio
            y, sr = librosa.load(file_path, sr=None)
            duration = librosa.get_duration(y=y, sr=sr)
            
            # 1. Detectar BPM
            tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
            bpm = float(tempo) if isinstance(tempo, (int, float, np.number)) else 95.0
            
            # 2. Detectar Key (simplificado - usa chroma)
            chroma = librosa.feature.chroma_stft(y=y, sr=sr)
            chroma_mean = np.mean(chroma, axis=1)
            key_idx = np.argmax(chroma_mean)
            keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
            key = keys[key_idx] + "m"  # Asumimos menor para reggaeton
            
            # 3. Energy curve (RMS por segmentos de 1 segundo)
            hop_length = 512
            frame_length = sr  # 1 segundo
            rms = librosa.feature.rms(y=y, frame_length=frame_length, hop_length=hop_length)[0]
            energy_curve = rms.tolist() if len(rms) > 0 else [0.5]
            
            # Normalizar a 16 segmentos máximo
            if len(energy_curve) > 16:
                # Agrupar en 16 segmentos
                segment_size = len(energy_curve) // 16
                energy_curve = [
                    np.mean(energy_curve[i:i+segment_size])
                    for i in range(0, len(energy_curve), segment_size)
                ][:16]
            
            # 4. MFCCs (timbre) - promedio
            mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
            mfccs_mean = np.mean(mfccs, axis=1).tolist()
            
            # 5. Spectral centroid (brillo)
            spectral_centroids = librosa.feature.spectral_centroid(y=y, sr=sr)[0]
            spectral_centroid_mean = float(np.mean(spectral_centroids))
            
            # 6. Onset strength (ritmo/percussividad)
            onset_env = librosa.onset.onset_strength(y=y, sr=sr)
            onset_strength_mean = float(np.mean(onset_env))
            
            logger.info("Análisis completado: %s (BPM: %.1f, Key: %s)", 
                       file_path, bpm, key)
            
            return SpectralFingerprint(
                bpm=bpm,
                key=key,
                energy_curve=energy_curve,
                mfccs_mean=mfccs_mean,
                spectral_centroid_mean=spectral_centroid_mean,
                onset_strength_mean=onset_strength_mean,
                duration=duration,
                sample_rate=sr
            )
            
        except Exception as e:
            logger.error("Error analizando %s: %s", file_path, e)
            return self._generate_mock_fingerprint(file_path)
    
    def _generate_mock_fingerprint(self, file_path: str) -> SpectralFingerprint:
        """Genera fingerprint simulado para pruebas sin librosa."""
        import hashlib
        
        # Generar valores deterministas basados en el nombre del archivo
        name_hash = hashlib.md5(file_path.encode()).hexdigest()
        
        # BPM entre 85-105 (típico reggaeton)
        bpm = 85 + (int(name_hash[:4], 16) % 20)
        
        # Key basada en hash
        keys = ['Am', 'Dm', 'Gm', 'Cm', 'Em', 'Bm', 'Fm']
        key = keys[int(name_hash[4:6], 16) % len(keys)]
        
        # Energy curve simulado (16 segmentos)
        import random as _random
        _random.seed(int(name_hash[:8], 16))
        energy_curve = [_random.uniform(0.3, 0.9) for _ in range(16)]
        
        # MFCCs simulados
        mfccs_mean = [_random.uniform(-50, 50) for _ in range(13)]
        
        return SpectralFingerprint(
            bpm=float(bpm),
            key=key,
            energy_curve=energy_curve,
            mfccs_mean=mfccs_mean,
            spectral_centroid_mean=float(2000 + int(name_hash[6:10], 16) % 2000),
            onset_strength_mean=float(0.3 + (int(name_hash[10:12], 16) % 70) / 100),
            duration=30.0,
            sample_rate=44100
        )


class SimilarityEngine:
    """Calcula similitud entre fingerprints espectrales."""
    
    def find_similar(self, 
                    reference: SpectralFingerprint,
                    candidates: List[Tuple[str, SpectralFingerprint]],
                    top_k: int = 20) -> List[SampleMatch]:
        """
        Encuentra los samples más similares a la referencia.
        
        Args:
            reference: Fingerprint de referencia
            candidates: Lista de (path, fingerprint) a comparar
            top_k: Número de resultados a retornar
            
        Returns:
            Lista de SampleMatch ordenados por similitud
        """
        matches = []
        
        for path, candidate_fp in candidates:
            score = self._calculate_similarity(reference, candidate_fp)
            
            # Determinar rol basado en path
            role = self._guess_role_from_path(path)
            name = os.path.basename(path)
            
            matches.append(SampleMatch(
                path=path,
                name=name,
                role=role,
                similarity_score=score,
                fingerprint=candidate_fp
            ))
        
        # Ordenar por score descendente
        matches.sort(key=lambda x: x.similarity_score, reverse=True)
        
        return matches[:top_k]
    
    def _calculate_similarity(self, 
                             ref: SpectralFingerprint, 
                             cand: SpectralFingerprint) -> float:
        """
        Calcula score de similitud entre dos fingerprints.
        Retorna valor entre 0.0 y 1.0.
        """
        scores = []
        weights = []
        
        # 1. Similitud de BPM (weight: 0.25)
        if ref.bpm > 0 and cand.bpm > 0:
            bpm_diff = abs(ref.bpm - cand.bpm)
            bpm_sim = max(0, 1 - (bpm_diff / 30))  # 30 BPM de tolerancia
            scores.append(bpm_sim)
            weights.append(0.25)
        
        # 2. Similitud de Key (weight: 0.15)
        if ref.key and cand.key:
            key_sim = 1.0 if ref.key == cand.key else 0.5 if ref.key[0] == cand.key[0] else 0.0
            scores.append(key_sim)
            weights.append(0.15)
        
        # 3. Similitud de Energy Curve (weight: 0.25)
        if ref.energy_curve and cand.energy_curve:
            # Interpolar a mismo tamaño
            min_len = min(len(ref.energy_curve), len(cand.energy_curve))
            ref_curve = ref.energy_curve[:min_len]
            cand_curve = cand.energy_curve[:min_len]
            
            if NUMPY_AVAILABLE and np is not None:
                ref_arr = np.array(ref_curve)
                cand_arr = np.array(cand_curve)
                # Correlación de Pearson
                if len(ref_arr) > 1:
                    corr = np.corrcoef(ref_arr, cand_arr)[0, 1]
                    if not np.isnan(corr):
                        energy_sim = (corr + 1) / 2  # Normalizar a 0-1
                        scores.append(energy_sim)
                        weights.append(0.25)
            else:
                # Fallback: simple mean absolute difference
                if min_len > 0:
                    diff = sum(abs(a - b) for a, b in zip(ref_curve, cand_curve)) / min_len
                    energy_sim = max(0, 1 - diff)
                    scores.append(energy_sim)
                    weights.append(0.25)
        
        # 4. Similitud de Timbre (MFCCs) (weight: 0.20)
        if ref.mfccs_mean and cand.mfccs_mean:
            min_mfcc = min(len(ref.mfccs_mean), len(cand.mfccs_mean))
            ref_mfccs = ref.mfccs_mean[:min_mfcc]
            cand_mfccs = cand.mfccs_mean[:min_mfcc]
            
            if NUMPY_AVAILABLE and np is not None:
                ref_arr = np.array(ref_mfccs)
                cand_arr = np.array(cand_mfccs)
                # Distancia euclidiana normalizada
                distance = np.linalg.norm(ref_arr - cand_arr)
                max_dist = np.linalg.norm(np.abs(ref_arr) + 100)  # Estimación de max
                timbre_sim = max(0, 1 - (distance / max_dist))
            else:
                # Fallback: simple euclidean distance
                distance = sum((a - b) ** 2 for a, b in zip(ref_mfccs, cand_mfccs)) ** 0.5
                max_dist = sum((abs(a) + 100) ** 2 for a in ref_mfccs) ** 0.5
                timbre_sim = max(0, 1 - (distance / max_dist)) if max_dist > 0 else 0.5
            
            scores.append(timbre_sim)
            weights.append(0.20)
        
        # 5. Similitud de Spectral Centroid (weight: 0.10)
        if ref.spectral_centroid_mean > 0 and cand.spectral_centroid_mean > 0:
            sc_diff = abs(ref.spectral_centroid_mean - cand.spectral_centroid_mean)
            sc_max = max(ref.spectral_centroid_mean, cand.spectral_centroid_mean)
            sc_sim = max(0, 1 - (sc_diff / sc_max)) if sc_max > 0 else 0.5
            scores.append(sc_sim)
            weights.append(0.10)
        
        # 6. Similitud de Onset Strength (weight: 0.05)
        if ref.onset_strength_mean > 0 and cand.onset_strength_mean > 0:
            os_diff = abs(ref.onset_strength_mean - cand.onset_strength_mean)
            os_max = max(ref.onset_strength_mean, cand.onset_strength_mean)
            os_sim = max(0, 1 - (os_diff / os_max)) if os_max > 0 else 0.5
            scores.append(os_sim)
            weights.append(0.05)
        
        # Calcular promedio ponderado
        if not scores:
            return 0.5
        
        total_weight = sum(weights)
        weighted_score = sum(s * w for s, w in zip(scores, weights)) / total_weight
        
        return float(weighted_score)
    
    def _guess_role_from_path(self, path: str) -> str:
        """Infiere el rol del sample basado en su path."""
        lower = path.lower()
        
        if "kick" in lower:
            return "kick"
        if "snare" in lower:
            return "snare"
        if "clap" in lower:
            return "clap"
        if "hi-hat" in lower or "hihat" in lower:
            return "hat_closed"
        if "bass" in lower:
            return "bass"
        if "fx" in lower:
            return "fx"
        if "perc" in lower:
            return "perc"
        if "drumloop" in lower or "drum_loop" in lower:
            return "drum_loop"
        if "oneshot" in lower or "synth" in lower:
            return "synth"
        
        return "synth"  # Default


class ReferenceMatcher:
    """
    Matcher principal que analiza referencias y genera perfiles de usuario.
    """
    
    def __init__(self, 
                 reference_path: Optional[str] = None,
                 library_path: Optional[str] = None,
                 profile_path: Optional[str] = None):
        self.reference_path = reference_path or str(REFERENCE_FILE)
        self.library_path = library_path or str(REGGAETON_DIR)
        self.profile_path = profile_path or str(PROFILE_FILE)
        
        self.analyzer = AudioAnalyzer()
        self.similarity = SimilarityEngine()
        
        self._reference_fingerprint: Optional[SpectralFingerprint] = None
        self._library_index: List[Tuple[str, SpectralFingerprint]] = []
        self._profile: Optional[UserSoundProfile] = None
    
    def analyze_reference(self) -> Optional[SpectralFingerprint]:
        """
        Analiza el archivo de referencia y retorna su fingerprint.
        
        Returns:
            SpectralFingerprint del archivo de referencia
        """
        logger.info("Analizando referencia: %s", self.reference_path)
        
        self._reference_fingerprint = self.analyzer.analyze_file(self.reference_path)
        
        if self._reference_fingerprint:
            logger.info("Referencia analizada - BPM: %.1f, Key: %s", 
                       self._reference_fingerprint.bpm,
                       self._reference_fingerprint.key)
        
        return self._reference_fingerprint
    
    def index_library(self, force_reindex: bool = False) -> List[Tuple[str, SpectralFingerprint]]:
        """
        Indexa toda la librería y extrae fingerprints.
        
        Args:
            force_reindex: Si True, reindexa aunque ya exista índice
            
        Returns:
            Lista de (path, fingerprint) de todos los samples
        """
        if self._library_index and not force_reindex:
            return self._library_index
        
        logger.info("Indexando librería: %s", self.library_path)
        
        self._library_index = []
        library = Path(self.library_path)
        
        if not library.is_dir():
            logger.error("Librería no encontrada: %s", self.library_path)
            return []
        
        audio_extensions = ('.wav', '.aif', '.aiff', '.mp3', '.flac', '.ogg')
        
        for root, _dirs, files in os.walk(library):
            for filename in files:
                if filename.lower().endswith(audio_extensions):
                    filepath = os.path.join(root, filename)
                    
                    # Analizar sample
                    fingerprint = self.analyzer.analyze_file(filepath)
                    
                    if fingerprint:
                        self._library_index.append((filepath, fingerprint))
                        logger.debug("Indexado: %s", filename)
        
        logger.info("Librería indexada: %d samples", len(self._library_index))
        return self._library_index
    
    def find_similar_samples(self, 
                            top_k: int = 50,
                            role_filter: Optional[str] = None) -> List[SampleMatch]:
        """
        Encuentra los samples más similares a la referencia.
        
        Args:
            top_k: Número de samples a retornar
            role_filter: Si se especifica, filtra por rol específico
            
        Returns:
            Lista de SampleMatch ordenados por similitud
        """
        if not self._reference_fingerprint:
            self.analyze_reference()
        
        if not self._library_index:
            self.index_library()
        
        if not self._reference_fingerprint or not self._library_index:
            logger.error("No se puede buscar similares: falta referencia o librería")
            return []
        
        # Filtrar por rol si es necesario
        candidates = self._library_index
        if role_filter:
            candidates = [
                (path, fp) for path, fp in candidates
                if self.similarity._guess_role_from_path(path) == role_filter
            ]
        
        logger.info("Buscando %d samples similares (filtro: %s)...", 
                   top_k, role_filter or "ninguno")
        
        matches = self.similarity.find_similar(
            self._reference_fingerprint,
            candidates,
            top_k=top_k
        )
        
        return matches
    
    def generate_user_profile(self, 
                             top_matches_count: int = 100,
                             save: bool = True) -> UserSoundProfile:
        """
        Genera el perfil de sonido del usuario basado en matches similares.
        
        Args:
            top_matches_count: Cuántos matches usar para el perfil
            save: Si True, guarda el perfil en disco
            
        Returns:
            UserSoundProfile generado
        """
        logger.info("Generando perfil de usuario...")
        
        # Obtener matches
        matches = self.find_similar_samples(top_k=top_matches_count)
        
        if not matches:
            logger.warning("No hay matches para generar perfil")
            return UserSoundProfile()
        
        # Calcular BPM preferido (promedio ponderado por similitud)
        total_weight = sum(m.similarity_score for m in matches)
        weighted_bpm = sum(m.fingerprint.bpm * m.similarity_score 
                          for m in matches if m.fingerprint.bpm > 0)
        preferred_bpm = weighted_bpm / total_weight if total_weight > 0 else 95.0
        
        # Calcular Key preferida (moda)
        keys = [m.fingerprint.key for m in matches if m.fingerprint.key]
        preferred_key = Counter(keys).most_common(1)[0][0] if keys else "Am"
        
        # Calcular Timbre promedio (MFCCs ponderados)
        mfccs_list = []
        weights = []
        for m in matches:
            if m.fingerprint.mfccs_mean:
                mfccs_list.append(m.fingerprint.mfccs_mean)
                weights.append(m.similarity_score)
        
        if mfccs_list and weights and NUMPY_AVAILABLE and np is not None:
            mfccs_arrays = [np.array(m) for m in mfccs_list]
            weighted_mfccs = np.average(mfccs_arrays, axis=0, weights=weights)
            preferred_timbre = weighted_mfccs.tolist()
        elif mfccs_list and weights:
            # Fallback: simple weighted average without numpy
            n_features = len(mfccs_list[0])
            preferred_timbre = []
            for i in range(n_features):
                weighted_sum = sum(m[i] * w for m, w in zip(mfccs_list, weights))
                preferred_timbre.append(weighted_sum / sum(weights))
        else:
            preferred_timbre = []
        
        # Energy curve característico (promedio de los matches)
        energy_curves = []
        for m in matches:
            if m.fingerprint.energy_curve:
                energy_curves.append(m.fingerprint.energy_curve)
        
        if energy_curves:
            # Interpolar todos a 16 segmentos
            interpolated = []
            for ec in energy_curves:
                ec_list = list(ec)
                if len(ec_list) < 16:
                    # Replicar para llegar a 16
                    repeated = (ec_list * (16 // len(ec_list) + 1))[:16]
                    interpolated.append(repeated)
                else:
                    interpolated.append(ec_list[:16])
            
            if NUMPY_AVAILABLE and np is not None:
                char_energy_curve = np.mean(interpolated, axis=0).tolist()
            else:
                # Fallback: manual mean
                n_curves = len(interpolated)
                n_points = len(interpolated[0]) if interpolated else 0
                char_energy_curve = [
                    sum(interpolated[j][i] for j in range(n_curves)) / n_curves
                    for i in range(n_points)
                ] if n_points > 0 else [0.5] * 16
        else:
            char_energy_curve = [0.5] * 16
        
        # Roles más usados
        role_counts = Counter(m.role for m in matches)
        preferred_roles = [role for role, _ in role_counts.most_common()]
        
        # Top matches por rol
        top_by_role: Dict[str, List[Dict]] = {}
        for role in SAMPLE_ROLES:
            role_matches = [m for m in matches if m.role == role][:10]
            if role_matches:
                top_by_role[role] = [
                    {
                        "path": m.path,
                        "name": m.name,
                        "similarity_score": m.similarity_score,
                        "bpm": m.fingerprint.bpm,
                        "key": m.fingerprint.key
                    }
                    for m in role_matches
                ]
        
        # Crear perfil
        profile = UserSoundProfile(
            preferred_bpm=preferred_bpm,
            preferred_key=preferred_key,
            preferred_timbre=preferred_timbre,
            characteristic_energy_curve=char_energy_curve,
            preferred_roles=preferred_roles,
            created_from_reference=self.reference_path,
            total_matches_analyzed=len(matches),
            genre="reggaeton",
            top_matches_by_role=top_by_role
        )
        
        self._profile = profile
        
        if save:
            self._save_profile(profile)
        
        logger.info("Perfil generado - BPM: %.1f, Key: %s, Roles: %s",
                   preferred_bpm, preferred_key, preferred_roles[:5])
        
        return profile
    
    def _save_profile(self, profile: UserSoundProfile) -> bool:
        """Guarda el perfil en disco."""
        try:
            profile_data = profile.to_dict()
            
            with open(self.profile_path, 'w', encoding='utf-8') as f:
                json.dump(profile_data, f, indent=2, ensure_ascii=False)
            
            logger.info("Perfil guardado en: %s", self.profile_path)
            return True
            
        except Exception as e:
            logger.error("Error guardando perfil: %s", e)
            return False
    
    def load_profile(self) -> Optional[UserSoundProfile]:
        """
        Carga el perfil desde disco.
        
        Returns:
            UserSoundProfile o None si no existe
        """
        if not os.path.exists(self.profile_path):
            logger.info("No existe perfil guardado en: %s", self.profile_path)
            return None
        
        try:
            with open(self.profile_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            self._profile = UserSoundProfile.from_dict(data)
            logger.info("Perfil cargado desde: %s", self.profile_path)
            return self._profile
            
        except Exception as e:
            logger.error("Error cargando perfil: %s", e)
            return None
    
    def get_user_profile(self) -> UserSoundProfile:
        """
        Obtiene el perfil del usuario, cargándolo o generándolo si no existe.
        
        Returns:
            UserSoundProfile del usuario
        """
        # Intentar cargar
        profile = self.load_profile()
        
        if profile:
            self._profile = profile
            return profile
        
        # Generar nuevo
        logger.info("Generando nuevo perfil de usuario...")
        return self.generate_user_profile()
    
    def get_recommended_samples(self, 
                               role: str, 
                               count: int = 5,
                               bpm_tolerance: float = 5.0) -> List[Dict[str, Any]]:
        """
        Retorna samples recomendados basados en el perfil del usuario.
        
        Args:
            role: Rol del sample deseado (kick, snare, bass, etc.)
            count: Número de samples a retornar
            bpm_tolerance: Tolerancia de BPM para filtrar
            
        Returns:
            Lista de diccionarios con información de samples recomendados
        """
        # Asegurar que tenemos perfil
        if not self._profile:
            self.get_user_profile()
        
        profile = self._profile
        if not profile:
            logger.warning("No se pudo obtener perfil, usando recomendaciones genéricas")
            # Fallback: buscar similares sin perfil
            matches = self.find_similar_samples(top_k=count * 3, role_filter=role)
            return [
                {
                    "path": m.path,
                    "name": m.name,
                    "role": m.role,
                    "similarity_score": m.similarity_score,
                    "bpm": m.fingerprint.bpm,
                    "key": m.fingerprint.key,
                    "reason": "Similitud directa con referencia"
                }
                for m in matches[:count]
            ]
        
        # Buscar en top_matches_by_role del perfil
        if role in profile.top_matches_by_role:
            matches = profile.top_matches_by_role[role]
            
            # Filtrar por BPM dentro de tolerancia
            filtered = [
                m for m in matches
                if abs(m.get("bpm", 0) - profile.preferred_bpm) <= bpm_tolerance
            ]
            
            # Si no hay suficientes con BPM cercano, usar todos
            if len(filtered) < count:
                filtered = matches
            
            recommendations = filtered[:count]
            
            return [
                {
                    "path": r["path"],
                    "name": r["name"],
                    "role": role,
                    "similarity_score": r["similarity_score"],
                    "bpm": r.get("bpm", 0),
                    "key": r.get("key", ""),
                    "reason": f"Match con perfil (Key: {profile.preferred_key}, BPM: {profile.preferred_bpm:.1f})"
                }
                for r in recommendations
            ]
        
        # Si no hay matches en el perfil para este rol, buscar en tiempo real
        logger.info("No hay matches en perfil para '%s', buscando en librería...", role)
        matches = self.find_similar_samples(top_k=count * 2, role_filter=role)
        
        return [
            {
                "path": m.path,
                "name": m.name,
                "role": m.role,
                "similarity_score": m.similarity_score,
                "bpm": m.fingerprint.bpm,
                "key": m.fingerprint.key,
                "reason": "Búsqueda en tiempo real"
            }
            for m in matches[:count]
        ]
    
    def get_profile_summary(self) -> Dict[str, Any]:
        """
        Retorna resumen del perfil para debugging/visualización.
        
        Returns:
            Diccionario con resumen del perfil
        """
        if not self._profile:
            self.get_user_profile()
        
        if not self._profile:
            return {"error": "No se pudo generar perfil"}
        
        p = self._profile
        
        return {
            "preferred_bpm": round(p.preferred_bpm, 1),
            "preferred_key": p.preferred_key,
            "characteristic_energy_curve": [round(x, 3) for x in p.characteristic_energy_curve[:8]],
            "preferred_roles": p.preferred_roles[:5],
            "top_matches_by_role_count": {
                role: len(matches) 
                for role, matches in p.top_matches_by_role.items()
            },
            "total_matches_analyzed": p.total_matches_analyzed,
            "created_from": p.created_from_reference,
            "genre": p.genre
        }


# Funciones de conveniencia globales
_matcher: Optional[ReferenceMatcher] = None


def get_matcher(reference_path: Optional[str] = None,
                library_path: Optional[str] = None) -> ReferenceMatcher:
    """Obtiene instancia global del matcher."""
    global _matcher
    if _matcher is None:
        _matcher = ReferenceMatcher(reference_path, library_path)
    return _matcher


def get_user_profile(reference_path: Optional[str] = None,
                     library_path: Optional[str] = None) -> Dict[str, Any]:
    """
    Función principal: obtiene o genera el perfil del usuario.
    
    Args:
        reference_path: Ruta al archivo de referencia (opcional)
        library_path: Ruta a la librería de samples (opcional)
        
    Returns:
        Diccionario con el perfil del usuario
    """
    matcher = get_matcher(reference_path, library_path)
    profile = matcher.get_user_profile()
    return profile.to_dict()


def get_recommended_samples(role: str, 
                            count: int = 5,
                            reference_path: Optional[str] = None,
                            library_path: Optional[str] = None) -> List[Dict[str, Any]]:
    """
    Obtiene samples recomendados para un rol específico.
    
    Args:
        role: Rol del sample (kick, snare, bass, synth, etc.)
        count: Número de samples a retornar
        reference_path: Ruta al archivo de referencia (opcional)
        library_path: Ruta a la librería (opcional)
        
    Returns:
        Lista de samples recomendados
    """
    matcher = get_matcher(reference_path, library_path)
    return matcher.get_recommended_samples(role, count)


def analyze_reference(file_path: str) -> Optional[Dict[str, Any]]:
    """
    Analiza un archivo de referencia y retorna su fingerprint.
    
    Args:
        file_path: Ruta al archivo de audio
        
    Returns:
        Diccionario con el fingerprint o None si falla
    """
    analyzer = AudioAnalyzer()
    fingerprint = analyzer.analyze_file(file_path)
    
    if fingerprint:
        return fingerprint.to_dict()
    
    return None


def refresh_profile() -> Dict[str, Any]:
    """
    Fuerza la regeneración del perfil del usuario.
    
    Returns:
        Nuevo perfil generado
    """
    global _matcher
    _matcher = None  # Reset para forzar regeneración
    
    matcher = get_matcher()
    profile = matcher.generate_user_profile(save=True)
    
    return profile.to_dict()


if __name__ == "__main__":
    # Test del módulo
    logging.basicConfig(level=logging.INFO)
    
    print("=" * 60)
    print("Reference Matcher - Test")
    print("=" * 60)
    
    # Test 1: Analizar referencia
    print("\n1. Analizando referencia...")
    matcher = ReferenceMatcher()
    ref_fp = matcher.analyze_reference()
    
    if ref_fp:
        print(f"   BPM: {ref_fp.bpm}")
        print(f"   Key: {ref_fp.key}")
        print(f"   Duration: {ref_fp.duration:.2f}s")
    
    # Test 2: Indexar librería
    print("\n2. Indexando librería...")
    library = matcher.index_library()
    print(f"   Samples indexados: {len(library)}")
    
    # Test 3: Generar perfil
    print("\n3. Generando perfil de usuario...")
    profile = matcher.generate_user_profile(top_matches_count=30)
    print(f"   Preferred BPM: {profile.preferred_bpm:.1f}")
    print(f"   Preferred Key: {profile.preferred_key}")
    print(f"   Preferred Roles: {profile.preferred_roles[:3]}")
    
    # Test 4: Recomendaciones
    print("\n4. Obteniendo recomendaciones...")
    for role in ["kick", "snare", "bass"]:
        recs = matcher.get_recommended_samples(role, count=2)
        print(f"   {role}: {[r['name'] for r in recs]}")
    
    print("\n" + "=" * 60)
    print("Test completado!")
    print("=" * 60)
