"""
audio_analyzer.py - AnÃ¡lisis de audio para detecciÃ³n de Key y BPM

Proporciona anÃ¡lisis bÃ¡sico de archivos de audio para extraer:
- BPM (tempo) mediante detecciÃ³n de onset y autocorrelaciÃ³n
- Key (tonalidad) mediante anÃ¡lisis de cromagrama
- CaracterÃ­sticas espectrales para clasificaciÃ³n
"""

import os
import logging
import numpy as np
import subprocess
from pathlib import Path
from typing import Dict, Any, Optional, Tuple, List
from dataclasses import dataclass
from enum import Enum

logger = logging.getLogger("AudioAnalyzer")

# Constantes musicales
NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
KEY_PROFILES = {
    # Perfiles de Krumhansl-Schmuckler para detecciÃ³n de tonalidad
    'major': [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88],
    'minor': [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
}

CIRCLE_OF_FIFTHS_MAJOR = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
CIRCLE_OF_FIFTHS_MINOR = ['Am', 'Em', 'Bm', 'F#m', 'C#m', 'G#m', 'D#m', 'A#m', 'Fm', 'Cm', 'Gm', 'Dm']


class SampleType(Enum):
    """Tipos de samples musicales"""
    KICK = "kick"
    SNARE = "snare"
    CLAP = "clap"
    HAT_CLOSED = "hat_closed"
    HAT_OPEN = "hat_open"
    HAT = "hat"
    PERC = "perc"
    SHAKER = "shaker"
    TOM = "tom"
    CRASH = "crash"
    RIDE = "ride"
    BASS = "bass"
    SYNTH = "synth"
    PAD = "pad"
    LEAD = "lead"
    PLUCK = "pluck"
    ARP = "arp"
    CHORD = "chord"
    STAB = "stab"
    VOCAL = "vocal"
    FX = "fx"
    LOOP = "loop"
    AMBIENCE = "ambience"
    UNKNOWN = "unknown"


@dataclass
class AudioFeatures:
    """CaracterÃ­sticas extraÃ­das de un archivo de audio"""
    bpm: Optional[float]
    key: Optional[str]
    key_confidence: float
    duration: float
    sample_rate: int
    sample_type: SampleType
    spectral_centroid: float
    spectral_rolloff: float
    zero_crossing_rate: float
    rms_energy: float
    is_harmonic: bool
    is_percussive: bool
    suggested_genres: List[str]
    # T115: Groove template from transient analysis
    groove_template: Optional[Dict[str, Any]] = None
    transients: Optional[List[float]] = None  # Transient positions in seconds
    onsets: Optional[List[float]] = None  # Onset detection results


class AudioAnalyzer:
    """
    Analizador de audio para samples musicales.

    Soporta mÃºltiples backends:
    - librosa (recomendado, mÃ¡s preciso)
    - basic (fallback sin dependencias externas, basado en nombre de archivo)
    """

    def __init__(self, backend: str = "auto"):
        """
        Inicializa el analizador de audio.

        Args:
            backend: 'librosa', 'basic', o 'auto' (detecta automÃ¡ticamente)
        """
        self.backend = backend
        self._librosa_available = False
        self._soundfile_available = False

        if backend in ("auto", "librosa"):
            self._check_librosa()

        if self._librosa_available:
            logger.info("Usando backend: librosa")
        else:
            logger.info("Usando backend: basic (anÃ¡lisis por nombre de archivo)")

    def _check_librosa(self):
        """Verifica si librosa estÃ¡ disponible"""
        try:
            import librosa
            import soundfile as sf
            self._librosa_available = True
            self._soundfile_available = True
            self.librosa = librosa
            self.sf = sf
        except ImportError:
            self._librosa_available = False
            self._soundfile_available = False

    def analyze(self, file_path: str) -> AudioFeatures:
        """
        Analiza un archivo de audio y extrae caracterÃ­sticas.

        Args:
            file_path: Ruta al archivo de audio

        Returns:
            AudioFeatures con los datos extraÃ­dos
        """
        path = Path(file_path)

        if not path.exists():
            raise FileNotFoundError(f"Archivo no encontrado: {file_path}")

        # Intentar anÃ¡lisis con librosa si estÃ¡ disponible
        if self._librosa_available:
            try:
                return self._analyze_with_librosa(file_path)
            except Exception as e:
                logger.warning(f"Error con librosa: {e}, usando anÃ¡lisis bÃ¡sico")

        # Fallback a anÃ¡lisis bÃ¡sico
        return self._analyze_basic(file_path)

    def _analyze_with_librosa(self, file_path: str) -> AudioFeatures:
        """AnÃ¡lisis completo usando librosa"""
        # Cargar audio
        y, sr = self.librosa.load(file_path, sr=None, mono=True)

        # DuraciÃ³n
        duration = self.librosa.get_duration(y=y, sr=sr)

        # Detectar BPM
        tempo, _ = self.librosa.beat.beat_track(y=y, sr=sr)
        bpm = float(tempo) if isinstance(tempo, (int, float, np.number)) else None

        # AnÃ¡lisis espectral
        spectral_centroids = self.librosa.feature.spectral_centroid(y=y, sr=sr)[0]
        spectral_rolloffs = self.librosa.feature.spectral_rolloff(y=y, sr=sr)[0]
        zcr = self.librosa.feature.zero_crossing_rate(y)[0]
        rms = self.librosa.feature.rms(y=y)[0]

        # Detectar key
        key, key_confidence = self._detect_key_librosa(y, sr)

        # ClasificaciÃ³n percusivo vs armÃ³nico
        is_percussive = self._is_percussive(y, sr)
        is_harmonic = not is_percussive and duration > 1.0

        # Determinar tipo de sample
        sample_type = self._classify_sample_type(
            file_path, is_percussive, is_harmonic, duration,
            float(np.mean(spectral_centroids)), float(np.mean(rms))
        )

        # Sugerir gÃ©neros
        suggested_genres = self._suggest_genres(sample_type, bpm, key)

        # T115: Detect transients and extract groove for drum loops
        groove_template = None
        transients = None
        onsets = None
        if sample_type in [SampleType.LOOP, SampleType.KICK, SampleType.SNARE, SampleType.CLAP, SampleType.HAT]:
            transients = self._detect_transients_librosa(y, sr)
            onsets = self._detect_onsets_librosa(y, sr)
            if transients and len(transients) > 0:
                groove_template = self._extract_groove_template(
                    y,
                    sr,
                    transients,
                    sample_type,
                    bpm=bpm,
                )
                logger.info(f"Extracted groove template with {len(transients)} transients")

        return AudioFeatures(
            bpm=bpm,
            key=key,
            key_confidence=key_confidence,
            duration=duration,
            sample_rate=sr,
            sample_type=sample_type,
            spectral_centroid=float(np.mean(spectral_centroids)),
            spectral_rolloff=float(np.mean(spectral_rolloffs)),
            zero_crossing_rate=float(np.mean(zcr)),
            rms_energy=float(np.mean(rms)),
            is_harmonic=is_harmonic,
            is_percussive=is_percussive,
            suggested_genres=suggested_genres,
            groove_template=groove_template,
            transients=transients,
            onsets=onsets
        )

    def _detect_key_librosa(self, y: np.ndarray, sr: int) -> Tuple[Optional[str], float]:
        """
        Detecta la tonalidad usando cromagrama y correlaciÃ³n con perfiles.
        """
        try:
            # Calcular cromagrama
            chroma = self.librosa.feature.chroma_stft(y=y, sr=sr)
            chroma_avg = np.mean(chroma, axis=1)

            # Normalizar
            chroma_avg = chroma_avg / (np.sum(chroma_avg) + 1e-10)

            best_key = None
            best_score = -np.inf
            best_mode = None

            # Probar todas las tonalidades mayores y menores
            for mode, profile in KEY_PROFILES.items():
                for i in range(12):
                    # Rotar el perfil
                    rotated_profile = np.roll(profile, i)
                    # CorrelaciÃ³n
                    score = np.corrcoef(chroma_avg, rotated_profile)[0, 1]

                    if score > best_score:
                        best_score = score
                        best_mode = mode
                        best_key = NOTE_NAMES[i]

            # Formatear resultado
            if best_key:
                if best_mode == 'minor':
                    best_key = best_key + 'm'
                confidence = max(0.0, min(1.0, (best_score + 1) / 2))
                return best_key, confidence

        except Exception as e:
            logger.warning(f"Error detectando key: {e}")

        return None, 0.0

    def _is_percussive(self, y: np.ndarray, sr: int) -> bool:
        """
        Determina si un sonido es principalmente percusivo.
        """
        try:
            # Separar componentes armÃ³nicos y percusivos
            y_harmonic, y_percussive = self.librosa.effects.hpss(y)

            # Calcular energÃ­a relativa
            energy_harmonic = np.sum(y_harmonic ** 2)
            energy_percussive = np.sum(y_percussive ** 2)
            total_energy = energy_harmonic + energy_percussive

            if total_energy > 0:
                percussive_ratio = energy_percussive / total_energy
                return percussive_ratio > 0.6

        except Exception as e:
            logger.warning(f"Error en separaciÃ³n HPSS: {e}")

        # Fallback: usar duraciÃ³n como heurÃ­stica
        duration = len(y) / sr
        return duration < 0.5

    def _detect_transients_librosa(self, y: np.ndarray, sr: int) -> List[float]:
        """
        T115: Detecta transientes usando onset detection de librosa.
        Retorna lista de posiciones en segundos.
        """
        try:
            # Compute onset envelope
            onset_env = self.librosa.onset.onset_strength(y=y, sr=sr)

            # Detect onset frames
            onset_frames = self.librosa.onset.onset_detect(
                onset_envelope=onset_env,
                sr=sr,
                wait=3,  # Minimum 3 frames between onsets
                pre_max=3,
                post_max=3,
                pre_avg=3,
                post_avg=5,
                delta=0.07,
                backtrack=False
            )

            # Convert frames to seconds
            onset_times = self.librosa.frames_to_time(onset_frames, sr=sr)

            # Filter by RMS energy to remove weak onsets
            rms = self.librosa.feature.rms(y=y)[0]
            rms_times = self.librosa.frames_to_time(np.arange(len(rms)), sr=sr)

            threshold = np.mean(rms) * 0.3  # Adaptive threshold

            filtered_onsets = []
            for onset_time in onset_times:
                # Find closest RMS frame
                rms_idx = np.argmin(np.abs(rms_times - onset_time))
                if rms_idx < len(rms) and rms[rms_idx] > threshold:
                    filtered_onsets.append(float(onset_time))

            return filtered_onsets

        except Exception as e:
            logger.warning(f"Error detectando transientes: {e}")
            return []

    def _detect_onsets_librosa(self, y: np.ndarray, sr: int) -> List[float]:
        """
        T115: Detecta onsets mÃ¡s sensibles (incluye notas mÃ¡s dÃ©biles).
        """
        try:
            onset_env = self.librosa.onset.onset_strength(y=y, sr=sr)
            onset_frames = self.librosa.onset.onset_detect(
                onset_envelope=onset_env,
                sr=sr,
                delta=0.03,  # More sensitive
                wait=2
            )
            return list(self.librosa.frames_to_time(onset_frames, sr=sr))
        except Exception as e:
            logger.warning(f"Error detectando onsets: {e}")
            return []

    def _estimate_beat_duration(self,
                                duration: float,
                                transients: List[float],
                                bpm: Optional[float] = None) -> float:
        """Estimate beat duration in seconds using BPM first, then transient spacing."""
        try:
            bpm_value = float(bpm or 0.0)
        except (TypeError, ValueError):
            bpm_value = 0.0

        if 60.0 <= bpm_value <= 200.0:
            return 60.0 / bpm_value

        ordered = sorted(float(t) for t in transients if t is not None)
        if len(ordered) >= 2:
            intervals = np.diff(np.asarray(ordered, dtype=float))
            intervals = intervals[(intervals >= 0.08) & (intervals <= 1.5)]
            if len(intervals) > 0:
                beat_duration = float(np.median(intervals))
                while beat_duration > 0.9:
                    beat_duration /= 2.0
                while beat_duration < 0.25:
                    beat_duration *= 2.0
                return beat_duration

        # Fallback conservador para loops de un compas.
        return max(0.25, min(1.0, duration / 4.0))

    def _extract_groove_template(self,
                                  y: np.ndarray,
                                  sr: int,
                                  transients: List[float],
                                  sample_type: SampleType,
                                  bpm: Optional[float] = None) -> Optional[Dict[str, Any]]:
        """
        T115: Extrae template de groove a partir de transientes detectados.

        Analiza la densidad, timing y velocidades relativas para crear
        un template que puede aplicarse a generaciÃ³n de patrones.
        """
        try:
            if not transients or len(transients) < 2:
                return None

            # Calculate duration
            duration = len(y) / sr

            transients = sorted(float(t) for t in transients if t is not None)
            beat_duration = self._estimate_beat_duration(duration, transients, bpm=bpm)
            subdivision_duration = max(beat_duration / 4.0, 1e-4)

            # Analyze amplitude at each transient for velocity
            velocities = []
            for t in transients:
                # Get sample index
                idx = int(t * sr)
                if idx < len(y) - 100:
                    # Calculate local RMS around transient
                    window = y[idx:idx+100]
                    rms_local = np.sqrt(np.mean(window**2))
                    velocities.append(float(rms_local))
                else:
                    velocities.append(0.5)

            # Normalize velocities
            if velocities and max(velocities) > 0:
                max_vel = max(velocities)
                velocities = [v / max_vel for v in velocities]

            # Calculate relative positions within bar (assuming 4 beats)
            bar_duration = beat_duration * 4
            positions = []
            for t in transients:
                # Normalize to 0-4 beat position inside one bar.
                rel_pos = (t % bar_duration) / beat_duration
                positions.append(round(rel_pos, 3))

            # Calculate density (transients per beat)
            density = len(transients) / max(duration / beat_duration, 1e-6)

            # Calculate timing variance against a 16th-note grid.
            ideal_beats = np.arange(0.0, duration + subdivision_duration, subdivision_duration)
            timing_offsets = []
            for t in transients:
                # Find closest rhythmic subdivision
                closest_beat = min(ideal_beats, key=lambda b: abs(b - t))
                offset = t - closest_beat
                timing_offsets.append(offset)

            timing_variance = np.std(timing_offsets) if timing_offsets else 0.0

            # Categorize by velocity into kick/snare/hat-like transients
            # High velocity = kick-like, medium = snare/clap, low = hat
            sorted_velocities = sorted(velocities, reverse=True)
            vel_threshold_high = sorted_velocities[len(sorted_velocities)//3] if len(sorted_velocities) >= 3 else 0.7
            vel_threshold_low = sorted_velocities[-len(sorted_velocities)//3] if len(sorted_velocities) >= 3 else 0.3

            kick_positions = []
            snare_positions = []
            hat_positions = []

            for pos, vel in zip(positions, velocities):
                if vel >= vel_threshold_high:
                    kick_positions.append(pos)
                elif vel >= vel_threshold_low:
                    snare_positions.append(pos)
                else:
                    hat_positions.append(pos)

            groove_template = {
                'positions': positions,
                'velocities': velocities,
                'density': float(density),
                'timing_variance_ms': float(timing_variance * 1000),
                'beat_duration': float(beat_duration),
                'duration': float(duration),
                'kick_positions': kick_positions,
                'snare_positions': snare_positions,
                'hat_positions': hat_positions,
                'extracted_from': str(sample_type.value),
            }

            return groove_template

        except Exception as e:
            logger.warning(f"Error extrayendo groove template: {e}")
            return None

    def _analyze_basic(self, file_path: str) -> AudioFeatures:
        """
        AnÃ¡lisis bÃ¡sico sin dependencias externas.
        Usa metadatos del archivo y nombre para inferir caracterÃ­sticas.
        """
        path = Path(file_path)
        name = path.stem

        # Extraer del nombre
        bpm = self._extract_bpm_from_name(name)
        key = self._extract_key_from_name(name)

        # Estimar duraciÃ³n del archivo
        duration = self._estimate_duration(file_path)

        # Clasificar por nombre
        sample_type = self._classify_by_name(name)

        # Determinar caracterÃ­sticas por tipo
        is_percussive = sample_type in [
            SampleType.KICK, SampleType.SNARE, SampleType.CLAP,
            SampleType.HAT, SampleType.HAT_CLOSED, SampleType.HAT_OPEN,
            SampleType.PERC, SampleType.SHAKER, SampleType.TOM,
            SampleType.CRASH, SampleType.RIDE
        ]
        is_harmonic = sample_type in [
            SampleType.BASS, SampleType.SYNTH, SampleType.PAD,
            SampleType.LEAD, SampleType.PLUCK, SampleType.CHORD,
            SampleType.VOCAL
        ]

        # Valores por defecto basados en tipo
        spectral_centroid = 5000.0 if is_percussive else 1000.0
        rms_energy = 0.5

        suggested_genres = self._suggest_genres(sample_type, bpm, key)

        return AudioFeatures(
            bpm=bpm,
            key=key,
            key_confidence=0.7 if key else 0.0,
            duration=duration,
            sample_rate=44100,
            sample_type=sample_type,
            spectral_centroid=spectral_centroid,
            spectral_rolloff=spectral_centroid * 2,
            zero_crossing_rate=0.1 if is_harmonic else 0.3,
            rms_energy=rms_energy,
            is_harmonic=is_harmonic,
            is_percussive=is_percussive,
            suggested_genres=suggested_genres
        )

    def _estimate_duration(self, file_path: str) -> float:
        """Estima la duraciÃ³n del archivo de audio"""
        try:
            import wave

            ext = Path(file_path).suffix.lower()

            if ext == '.wav':
                with wave.open(file_path, 'rb') as wav:
                    frames = wav.getnframes()
                    rate = wav.getframerate()
                    return frames / float(rate)

            elif ext in ('.mp3', '.ogg', '.flac', '.aif', '.aiff', '.m4a'):
                windows_duration = self._estimate_duration_with_windows_shell(file_path)
                if windows_duration > 0:
                    return windows_duration
                # EstimaciÃ³n por tamaÃ±o de archivo
                size = os.path.getsize(file_path)
                # AproximaciÃ³n: ~176KB por segundo para CD quality stereo
                return size / (176.4 * 1024)

        except Exception as e:
            logger.warning(f"Error estimando duraciÃ³n: {e}")

        return 0.0

    def _estimate_duration_with_windows_shell(self, file_path: str) -> float:
        """Obtiene la duraciÃ³n usando metadatos del shell de Windows cuando estÃ¡n disponibles."""
        if os.name != 'nt':
            return 0.0

        safe_path = file_path.replace("'", "''")
        powershell_command = (
            f"$path = '{safe_path}'; "
            "$shell = New-Object -ComObject Shell.Application; "
            "$folder = $shell.Namespace((Split-Path $path)); "
            "$file = $folder.ParseName((Split-Path $path -Leaf)); "
            "$duration = $folder.GetDetailsOf($file, 27); "
            "Write-Output $duration"
        )
        try:
            result = subprocess.run(
                f'powershell -NoProfile -Command "{powershell_command}"',
                capture_output=True,
                text=True,
                timeout=5,
                check=False,
                shell=True,
            )
            value = (result.stdout or "").strip()
            if not value:
                return 0.0
            parts = value.split(':')
            if len(parts) == 3:
                return (int(parts[0]) * 3600) + (int(parts[1]) * 60) + float(parts[2])
            return 0.0
        except Exception:
            return 0.0

    def _extract_bpm_from_name(self, name: str) -> Optional[float]:
        """Extrae BPM del nombre del archivo"""
        import re

        patterns = [
            r'[_\s\-](\d{2,3})\s*BPM',
            r'[_\s\-](\d{2,3})[_\s\-]',
            r'(\d{2,3})bpm',
            r'[_\s\-](\d{2,3})\s*(?:BPM|bpm)?\s*(?:\.wav|\.mp3|\.aif)',
        ]

        for pattern in patterns:
            match = re.search(pattern, name, re.IGNORECASE)
            if match:
                bpm = int(match.group(1))
                if 60 <= bpm <= 200:
                    return float(bpm)

        return None

    def _extract_key_from_name(self, name: str) -> Optional[str]:
        """Extrae key del nombre del archivo"""
        import re

        patterns = [
            r'[_\s\-]([A-G][#b]?(?:m|min|minor)?)[_\s\-]',
            r'\bin\s+([A-G][#b]?(?:m|min|minor)?)\b',
            r'Key\s+([A-G][#b]?(?:m|min|minor)?)',
            r'[_\s\-]([A-G][#b]?)\s*(?:maj|major)?[_\s\-]',
        ]

        for pattern in patterns:
            match = re.search(pattern, name, re.IGNORECASE)
            if match:
                key = match.group(1)
                # Normalizar
                key = key.replace('b', '#').replace('Db', 'C#').replace('Eb', 'D#')
                key = key.replace('Gb', 'F#').replace('Ab', 'G#').replace('Bb', 'A#')

                # Detectar si es menor
                is_minor = 'm' in key.lower() or 'min' in key.lower()
                key = key.replace('min', '').replace('minor', '').replace('major', '')
                key = key.rstrip('mM')

                if is_minor:
                    key = key + 'm'

                return key

        return None

    def _classify_sample_type(self, file_path: str, is_percussive: bool,
                               is_harmonic: bool, duration: float,
                               spectral_centroid: float, rms: float) -> SampleType:
        """Clasifica el tipo de sample basado en caracterÃ­sticas"""
        # Primero intentar por nombre
        sample_type = self._classify_by_name(Path(file_path).stem)
        if sample_type != SampleType.UNKNOWN:
            return sample_type

        # ClasificaciÃ³n por caracterÃ­sticas de audio
        if is_percussive:
            if duration < 0.1:
                if spectral_centroid < 2000:
                    return SampleType.KICK
                elif spectral_centroid > 8000:
                    return SampleType.HAT_CLOSED
                else:
                    return SampleType.SNARE
            elif duration < 0.3:
                return SampleType.CLAP
            else:
                return SampleType.PERC

        elif is_harmonic:
            if spectral_centroid < 500:
                return SampleType.BASS
            elif duration > 4.0:
                return SampleType.PAD
            else:
                return SampleType.SYNTH

        return SampleType.UNKNOWN

    def _classify_by_name(self, name: str) -> SampleType:
        """Clasifica el tipo de sample basado en su nombre"""
        name_lower = name.lower()

        # Mapeo de palabras clave a tipos
        keywords = {
            SampleType.KICK: ['kick', 'bd', 'bass drum', 'kickdrum', 'kik'],
            SampleType.SNARE: ['snare', 'snr', 'sd', 'rim'],
            SampleType.CLAP: ['clap', 'clp', 'handclap'],
            SampleType.HAT_CLOSED: ['closed hat', 'closedhat', 'chh', 'closed'],
            SampleType.HAT_OPEN: ['open hat', 'openhat', 'ohh', 'open'],
            SampleType.HAT: ['hat', 'hihat', 'hi-hat', 'hh'],
            SampleType.PERC: ['perc', 'percussion', 'conga', 'bongo', 'timb'],
            SampleType.SHAKER: ['shaker', 'shake', 'tamb'],
            SampleType.TOM: ['tom', 'tomtom'],
            SampleType.CRASH: ['crash', 'cymbal'],
            SampleType.RIDE: ['ride'],
            SampleType.BASS: ['bass', 'bassline', 'sub', '808', 'reese'],
            SampleType.SYNTH: ['synth', 'lead', 'arp', 'sequence'],
            SampleType.PAD: ['pad', 'atmosphere', 'dron'],
            SampleType.PLUCK: ['pluck'],
            SampleType.CHORD: ['chord', 'stab'],
            SampleType.VOCAL: ['vocal', 'vox', 'voice', 'speech', 'talk'],
            SampleType.FX: ['fx', 'effect', 'sweep', 'riser', 'downlifter', 'impact', 'hit', 'noise'],
            SampleType.LOOP: ['loop', 'full', 'groove'],
        }

        for sample_type, words in keywords.items():
            for word in words:
                if word in name_lower:
                    return sample_type

        return SampleType.UNKNOWN

    def _suggest_genres(self, sample_type: SampleType, bpm: Optional[float],
                        key: Optional[str]) -> List[str]:
        """Sugiere gÃ©neros musicales apropiados para el sample"""
        genres = []

        if bpm:
            if 118 <= bpm <= 128:
                genres.extend(['house', 'tech-house', 'deep-house'])
            elif 124 <= bpm <= 132:
                genres.extend(['tech-house', 'techno'])
            elif 132 <= bpm <= 142:
                genres.extend(['techno', 'peak-time-techno'])
            elif 142 <= bpm <= 150:
                genres.extend(['trance', 'hard-techno'])
            elif 160 <= bpm <= 180:
                genres.extend(['drum-and-bass', 'neurofunk'])
            elif bpm < 118:
                genres.extend(['downtempo', 'ambient', 'lo-fi'])

        # Por tipo de sample
        if sample_type in [SampleType.KICK, SampleType.SNARE, SampleType.CLAP]:
            if not genres:
                genres = ['techno', 'house']
        elif sample_type == SampleType.BASS:
            if not genres:
                genres = ['techno', 'house', 'bass-music']
        elif sample_type in [SampleType.SYNTH, SampleType.PAD]:
            if not genres:
                genres = ['trance', 'progressive', 'ambient']

        return genres if genres else ['electronic']

    def get_compatible_key(self, key: str, shift: int = 0) -> str:
        """
        Obtiene una key compatible usando el cÃ­rculo de quintas.

        Args:
            key: Key original (ej: 'Am', 'F#m')
            shift: Desplazamiento en el cÃ­rculo (+1 = quinta arriba, -1 = quinta abajo)

        Returns:
            Key resultante
        """
        is_minor = key.endswith('m')
        root = key.rstrip('m')

        if root not in NOTE_NAMES:
            return key

        circle = CIRCLE_OF_FIFTHS_MINOR if is_minor else CIRCLE_OF_FIFTHS_MAJOR

        try:
            idx = circle.index(key)
            new_idx = (idx + shift) % 12
            return circle[new_idx]
        except ValueError:
            return key

    def calculate_key_compatibility(self, key1: str, key2: str) -> float:
        """
        Calcula la compatibilidad entre dos keys (0-1).

        Usa el cÃ­rculo de quintas: keys cercanas son mÃ¡s compatibles.
        """
        if key1 == key2:
            return 1.0

        # Normalizar
        def normalize(k):
            is_minor = k.endswith('m')
            root = k.rstrip('m')
            # Convertir bemoles a sostenidos
            root = root.replace('Db', 'C#').replace('Eb', 'D#')
            root = root.replace('Gb', 'F#').replace('Ab', 'G#').replace('Bb', 'A#')
            return root + ('m' if is_minor else '')

        k1 = normalize(key1)
        k2 = normalize(key2)

        if k1 == k2:
            return 1.0

        # Verificar si son modos diferentes de la misma nota
        if k1.rstrip('m') == k2.rstrip('m'):
            return 0.8  # Mismo root, diferente modo

        # Usar cÃ­rculo de quintas
        is_minor1 = k1.endswith('m')
        is_minor2 = k2.endswith('m')

        if is_minor1 != is_minor2:
            return 0.3  # Diferente modo, baja compatibilidad

        circle = CIRCLE_OF_FIFTHS_MINOR if is_minor1 else CIRCLE_OF_FIFTHS_MAJOR

        try:
            idx1 = circle.index(k1)
            idx2 = circle.index(k2)
            distance = min(abs(idx1 - idx2), 12 - abs(idx1 - idx2))

            # Compatibilidad decrece con la distancia
            compatibility = max(0.0, 1.0 - (distance * 0.2))
            return compatibility

        except ValueError:
            return 0.0


# Instancia global
_analyzer: Optional[AudioAnalyzer] = None


def get_analyzer() -> AudioAnalyzer:
    """Obtiene la instancia global del analizador"""
    global _analyzer
    if _analyzer is None:
        _analyzer = AudioAnalyzer()
    return _analyzer


def analyze_sample(file_path: str) -> Dict[str, Any]:
    """
    FunciÃ³n de conveniencia para analizar un sample.

    Returns:
        Diccionario con las caracterÃ­sticas del sample
    """
    analyzer = get_analyzer()
    features = analyzer.analyze(file_path)

    return {
        'bpm': features.bpm,
        'key': features.key,
        'key_confidence': features.key_confidence,
        'duration': features.duration,
        'sample_rate': features.sample_rate,
        'sample_type': features.sample_type.value,
        'spectral_centroid': features.spectral_centroid,
        'rms_energy': features.rms_energy,
        'is_harmonic': features.is_harmonic,
        'is_percussive': features.is_percussive,
        'suggested_genres': features.suggested_genres,
        'groove_template': features.groove_template,
        'transients': features.transients,
        'onsets': features.onsets,
    }


def quick_analyze(file_path: str) -> Dict[str, Any]:
    """
    AnÃ¡lisis rÃ¡pido basado solo en el nombre del archivo.
    No requiere dependencias externas.
    """
    analyzer = AudioAnalyzer(backend="basic")
    features = analyzer.analyze(file_path)

    return {
        'bpm': features.bpm,
        'key': features.key,
        'sample_type': features.sample_type.value,
        'suggested_genres': features.suggested_genres,
    }


# Testing
if __name__ == "__main__":
    import sys

    logging.basicConfig(level=logging.INFO)

    if len(sys.argv) < 2:
        print("Uso: python audio_analyzer.py <archivo_de_audio>")
        sys.exit(1)

    file_path = sys.argv[1]

    print(f"\nAnalizando: {file_path}")
    print("=" * 50)

    try:
        result = analyze_sample(file_path)

        print("\nResultados:")
        print(f"  BPM: {result['bpm'] or 'No detectado'}")
        print(f"  Key: {result['key'] or 'No detectado'} (confianza: {result['key_confidence']:.2f})")
        print(f"  DuraciÃ³n: {result['duration']:.2f}s")
        print(f"  Tipo: {result['sample_type']}")
        print(f"  GÃ©neros sugeridos: {', '.join(result['suggested_genres'])}")
        print(f"  Es percusivo: {result['is_percussive']}")
        print(f"  Es armÃ³nico: {result['is_harmonic']}")

    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)
