"""
sample_manager.py - Gestión completa de librería de samples

Proporciona:
- Indexación y escaneo de directorios de samples
- Clasificación automática por tipo, key, BPM
- Gestión de metadatos y tags
- Búsqueda avanzada con filtros múltiples
- Caché de índice para rendimiento
- Soporte para múltiples formatos (WAV, AIFF, MP3, OGG, FLAC)
"""

import json
import hashlib
import logging
import os
from pathlib import Path
from typing import Dict, List, Any, Optional, Tuple, Callable
from dataclasses import dataclass, field, asdict
from datetime import datetime
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

# Importar analizador de audio
try:
    from .audio_analyzer import AudioAnalyzer, SampleType, analyze_sample, quick_analyze
    AUDIO_ANALYSIS_AVAILABLE = True
except ImportError:
    try:
        from audio_analyzer import AudioAnalyzer, SampleType, analyze_sample, quick_analyze
        AUDIO_ANALYSIS_AVAILABLE = True
    except ImportError:
        AUDIO_ANALYSIS_AVAILABLE = False
        AudioAnalyzer = None
        SampleType = None
        analyze_sample = None
        quick_analyze = None

logger = logging.getLogger("SampleManager")

DEFAULT_PROGRAM_DATA_DIR = Path("C:/ProgramData/Ableton/Live 12 Suite/Resources/MIDI Remote Scripts")
DEFAULT_REGGAETON_DIR = DEFAULT_PROGRAM_DATA_DIR / "libreria" / "reggaeton"
DEFAULT_FALLBACK_DIR = DEFAULT_PROGRAM_DATA_DIR / "librerias" / "organized_samples"
DEFAULT_SAMPLE_MANAGER_DIR = DEFAULT_REGGAETON_DIR if DEFAULT_REGGAETON_DIR.exists() else DEFAULT_FALLBACK_DIR


def _json_safe(value: Any) -> Any:
    if isinstance(value, dict):
        return {key: _json_safe(item) for key, item in value.items()}
    if isinstance(value, list):
        return [_json_safe(item) for item in value]
    if hasattr(value, "item"):
        try:
            return value.item()
        except Exception:
            return value
    return value


@dataclass
class Sample:
    """Representa un sample en la librería"""
    id: str
    name: str
    path: str
    category: str
    subcategory: str
    sample_type: str
    key: Optional[str] = None
    bpm: Optional[float] = None
    duration: float = 0.0
    sample_rate: int = 44100
    channels: int = 2
    file_size: int = 0
    format: str = "wav"

    # Metadatos adicionales
    genres: List[str] = field(default_factory=list)
    tags: List[str] = field(default_factory=list)
    mood: str = ""
    energy: float = 0.5  # 0-1

    # Información de análisis
    analyzed: bool = False
    analysis_version: int = 0
    spectral_centroid: float = 0.0
    rms_energy: float = 0.0
    is_harmonic: bool = False
    is_percussive: bool = False

    # Metadatos del sistema
    date_added: str = field(default_factory=lambda: datetime.now().isoformat())
    date_modified: str = field(default_factory=lambda: datetime.now().isoformat())
    play_count: int = 0
    rating: int = 0  # 0-5

    def to_dict(self) -> Dict[str, Any]:
        """Convierte el sample a diccionario"""
        return _json_safe(asdict(self))

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'Sample':
        """Crea un Sample desde un diccionario"""
        # Filtrar solo los campos que existen en la clase
        valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
        filtered_data = {k: v for k, v in data.items() if k in valid_fields}
        return cls(**filtered_data)

    def get_display_name(self) -> str:
        """Nombre formateado para mostrar"""
        parts = [self.name]
        if self.key:
            parts.append(f"Key: {self.key}")
        if self.bpm:
            parts.append(f"{self.bpm:.1f} BPM")
        return " | ".join(parts)


class SampleManager:
    """
    Gestor principal de la librería de samples.

    Características:
    - Indexación recursiva de directorios
    - Clasificación automática por tipo
    - Detección de key y BPM (si librosa está disponible)
    - Búsqueda avanzada con múltiples filtros
    - Sistema de favoritos y ratings
    - Caché persistente en JSON
    """

    # Categorías principales y subcategorías
    CATEGORIES = {
        'drums': {
            'kick': ['kick', 'bd', 'bass drum', 'kickdrum'],
            'snare': ['snare', 'snr', 'sd', 'rimshot'],
            'clap': ['clap', 'clp', 'handclap'],
            'hat_closed': ['closed hat', 'chh', 'closed'],
            'hat_open': ['open hat', 'ohh', 'open'],
            'hat': ['hat', 'hihat', 'hi-hat'],
            'perc': ['perc', 'percussion', 'conga', 'bongo', 'timbale'],
            'shaker': ['shaker', 'tambourine', 'tamb'],
            'tom': ['tom', 'tomtom'],
            'cymbal': ['crash', 'ride', 'cymbal', 'china'],
        },
        'bass': {
            'sub': ['sub', 'subbass', '808'],
            'bassline': ['bassline', 'bass', 'reese'],
            'acid': ['acid', 'tb303', '303'],
        },
        'synths': {
            'lead': ['lead', 'solo', 'main'],
            'pad': ['pad', 'atmosphere', 'dron', 'ambient'],
            'pluck': ['pluck', 'arp', 'arpeggio'],
            'chord': ['chord', 'stab', 'hit'],
            'fx': ['fx', 'effect', 'sweep', 'riser', 'downlifter'],
        },
        'vocals': {
            'vocal': ['vocal', 'vox', 'voice'],
            'speech': ['speech', 'talk', 'phrase'],
            'chant': ['chant', 'shout', 'yell'],
        },
        'loops': {
            'drum_loop': ['drum loop', 'beat loop', 'groove'],
            'perc_loop': ['perc loop', 'percussion loop'],
            'bass_loop': ['bass loop', 'bassline loop'],
            'synth_loop': ['synth loop', 'lead loop'],
            'full_loop': ['full loop', 'complete loop'],
        },
        'one_shots': {
            'hit': ['hit', 'impact', 'sting'],
            'noise': ['noise', 'texture', 'grain'],
        }
    }

    # Mapeo de extensiones de archivo
    SUPPORTED_FORMATS = {'.wav', '.aif', '.aiff', '.mp3', '.ogg', '.flac', '.m4a'}
    IGNORED_SEGMENTS = {'(extra)', '.sample_cache', '__pycache__', 'documentation', 'installer'}

    # Géneros soportados con palabras clave
    GENRE_KEYWORDS = {
        'house': ['house', 'deep', 'soulful', 'garage', 'classic'],
        'techno': ['techno', 'industrial', 'detroit', 'berlin', 'acid'],
        'tech-house': ['tech house', 'tech-house', 'groovy', 'bouncy'],
        'trance': ['trance', 'progressive', 'uplifting', 'psy'],
        'drum-and-bass': ['drum and bass', 'dnb', 'neuro', 'liquid', 'jungle'],
        'hip-hop': ['hip hop', 'hiphop', 'trap', 'boom bap', 'lofi'],
        'reggaeton': ['reggaeton', 'dembow', 'perreo', 'urbano', 'dancehall', 'primer impacto'],
        'ambient': ['ambient', 'chillout', 'downtempo', 'meditation'],
        'edm': ['edm', 'electro', 'big room', 'festival'],
    }

    def __init__(self, base_dir: str, cache_dir: Optional[str] = None):
        """
        Inicializa el gestor de samples.

        Args:
            base_dir: Directorio raíz de la librería de samples
            cache_dir: Directorio para caché (default: base_dir/.sample_cache)
        """
        self.base_dir = Path(base_dir)
        self.cache_dir = Path(cache_dir) if cache_dir else self.base_dir / ".sample_cache"
        self.cache_dir.mkdir(exist_ok=True)

        self.samples: Dict[str, Sample] = {}
        self.index_file = self.cache_dir / "sample_library.json"
        self.stats_file = self.cache_dir / "library_stats.json"

        # Analizador de audio
        self.analyzer = AudioAnalyzer() if AUDIO_ANALYSIS_AVAILABLE else None

        # Locks para thread-safety
        self._lock = threading.RLock()
        self._index_dirty = False

        # Estadísticas
        self.stats = {
            'total_samples': 0,
            'total_size': 0,
            'by_category': defaultdict(int),
            'by_key': defaultdict(int),
            'by_bpm_range': defaultdict(int),
            'last_scan': None,
        }

        # Cargar índice existente
        self._load_index()

    def _generate_id(self, file_path: str) -> str:
        """Genera un ID único para un sample basado en su ruta"""
        return hashlib.md5(file_path.encode()).hexdigest()[:16]

    def _get_file_hash(self, file_path: Path) -> str:
        """Calcula hash del archivo para detectar cambios"""
        stat = file_path.stat()
        return hashlib.md5(f"{stat.st_size}_{stat.st_mtime}".encode()).hexdigest()

    def _should_ignore_path(self, file_path: Path) -> bool:
        segments = {part.strip().lower() for part in file_path.parts}
        return any(segment in segments for segment in self.IGNORED_SEGMENTS)

    def _build_context_text(self, file_path: Path) -> str:
        try:
            rel_path = file_path.relative_to(self.base_dir)
        except ValueError:
            rel_path = file_path
        parent_context = " ".join(part.replace("_", " ").replace("-", " ") for part in rel_path.parts[:-1])
        stem_context = file_path.stem.replace("_", " ").replace("-", " ")
        return f"{parent_context} {stem_context}".strip()

    def scan_directory(self, directory: Optional[str] = None,
                       recursive: bool = True,
                       analyze_audio: bool = False,
                       progress_callback: Optional[Callable[[int, int, str], None]] = None) -> Dict[str, Any]:
        """
        Escanear un directorio en busca de samples.

        Args:
            directory: Directorio a escanear (default: base_dir)
            recursive: Escanear subdirectorios
            analyze_audio: Analizar contenido de audio (más lento)
            progress_callback: Función llamada con (procesados, total, archivo_actual)

        Returns:
            Estadísticas del escaneo
        """
        scan_dir = Path(directory) if directory else self.base_dir

        if not scan_dir.exists():
            raise FileNotFoundError(f"Directorio no encontrado: {scan_dir}")

        logger.info(f"Escaneando: {scan_dir}")

        # Encontrar todos los archivos de audio
        if recursive:
            audio_files = list(scan_dir.rglob('*'))
        else:
            audio_files = list(scan_dir.iterdir())

        audio_files = [f for f in audio_files
                       if f.is_file()
                       and f.suffix.lower() in self.SUPPORTED_FORMATS
                       and not self._should_ignore_path(f)]

        audio_files = sorted(audio_files, key=lambda item: str(item).lower())
        total = len(audio_files)
        processed = 0
        added = 0
        updated = 0
        errors = 0

        logger.info(f"Encontrados {total} archivos de audio")
        max_workers = max(1, (os.cpu_count() or 2) // 2)
        logger.info(f"Usando hasta {max_workers} workers para escaneo/análisis")

        if analyze_audio and total > 1 and max_workers > 1:
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                future_map = {
                    executor.submit(self._process_file, file_path, analyze_audio): file_path
                    for file_path in audio_files
                }
                for future in as_completed(future_map):
                    file_path = future_map[future]
                    processed += 1

                    if progress_callback:
                        progress_callback(processed, total, str(file_path.name))

                    try:
                        result = future.result()
                        if result == 'added':
                            added += 1
                        elif result == 'updated':
                            updated += 1
                    except Exception as e:
                        logger.error(f"Error procesando {file_path}: {e}")
                        errors += 1
        else:
            for file_path in audio_files:
                processed += 1

                if progress_callback:
                    progress_callback(processed, total, str(file_path.name))

                try:
                    result = self._process_file(file_path, analyze_audio)
                    if result == 'added':
                        added += 1
                    elif result == 'updated':
                        updated += 1

                except Exception as e:
                    logger.error(f"Error procesando {file_path}: {e}")
                    errors += 1

        with self._lock:
            self._index_dirty = True
            self._update_stats()
            self._save_index()

        self.stats['last_scan'] = datetime.now().isoformat()

        return {
            'processed': processed,
            'added': added,
            'updated': updated,
            'errors': errors,
            'total_samples': len(self.samples),
        }

    def _process_file(self, file_path: Path, analyze_audio: bool) -> str:
        """Procesa un archivo individual. Retorna 'added', 'updated', o 'unchanged'"""
        file_id = self._generate_id(str(file_path))

        # Verificar si ya existe y no ha cambiado
        with self._lock:
            existing = self.samples.get(file_id)
        if existing is not None:
            # Comparar hash implícito por fecha de modificación
            current_stat = file_path.stat()
            if existing.date_modified:
                try:
                    mod_time = datetime.fromisoformat(existing.date_modified).timestamp()
                    if abs(current_stat.st_mtime - mod_time) < 1:
                        return 'unchanged'
                except Exception:
                    pass

        # Extraer información del nombre
        name = file_path.stem
        context_text = self._build_context_text(file_path)
        category, subcategory = self._classify_by_name(context_text)
        sample_type = self._detect_sample_type(context_text)
        key = self._extract_key_from_name(context_text)
        bpm = self._extract_bpm_from_name(context_text)
        genres = self._detect_genres(context_text)

        # Análisis de audio si está disponible
        audio_features = {}
        if analyze_audio and self.analyzer:
            try:
                audio_features = analyze_sample(str(file_path))
                # Usar valores detectados si no están en el nombre
                if not bpm and audio_features.get('bpm'):
                    bpm = audio_features['bpm']
                if not key and audio_features.get('key'):
                    key = audio_features['key']
                if audio_features.get('sample_type'):
                    sample_type = audio_features['sample_type']
                if audio_features.get('suggested_genres'):
                    genres = list(set(genres + audio_features['suggested_genres']))
            except Exception as e:
                logger.warning(f"Error analizando audio {file_path}: {e}")

        # Crear o actualizar sample
        is_new = file_id not in self.samples

        sample = Sample(
            id=file_id,
            name=name,
            path=str(file_path),
            category=category,
            subcategory=subcategory,
            sample_type=sample_type,
            key=key,
            bpm=bpm,
            duration=audio_features.get('duration', 0.0),
            sample_rate=audio_features.get('sample_rate', 44100),
            file_size=file_path.stat().st_size,
            format=file_path.suffix.lower().lstrip('.'),
            genres=genres,
            tags=self._extract_tags(context_text),
            energy=max(0.0, min(1.0, float(audio_features.get('rms_energy', 0.5) or 0.5))),
            analyzed=analyze_audio,
            spectral_centroid=audio_features.get('spectral_centroid', 0.0),
            rms_energy=audio_features.get('rms_energy', 0.0),
            is_harmonic=audio_features.get('is_harmonic', False),
            is_percussive=audio_features.get('is_percussive', False),
            date_modified=datetime.now().isoformat(),
        )

        with self._lock:
            self.samples[file_id] = sample
        return 'added' if is_new else 'updated'

    def _classify_by_name(self, name: str) -> Tuple[str, str]:
        """Clasifica un sample por su nombre en categoría y subcategoría"""
        name_lower = name.lower()

        for category, subcategories in self.CATEGORIES.items():
            for subcategory, keywords in subcategories.items():
                for keyword in keywords:
                    if keyword in name_lower:
                        return category, subcategory

        # Fallback: intentar detectar loops
        if 'loop' in name_lower:
            return 'loops', 'unknown'

        return 'unknown', 'unknown'

    def _detect_sample_type(self, name: str) -> str:
        """Detecta el tipo específico de sample"""
        category, subcategory = self._classify_by_name(name)

        if category == 'drums':
            return subcategory
        elif category == 'bass':
            return f"bass_{subcategory}"
        elif category == 'synths':
            return subcategory
        elif category == 'vocals':
            return subcategory
        elif category == 'loops':
            return subcategory

        return 'unknown'

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

        # Patrones comunes
        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 bemoles a sostenidos
                key = key.replace('b', '#').replace('Db', 'C#').replace('Eb', 'D#')
                key = key.replace('Gb', 'F#').replace('Ab', 'G#').replace('Bb', 'A#')

                # Detectar modo
                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 _extract_bpm_from_name(self, name: str) -> Optional[float]:
        """Extrae el 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',
        ]

        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 _detect_genres(self, name: str) -> List[str]:
        """Detecta géneros musicales del nombre"""
        name_lower = name.lower()
        genres = []

        for genre, keywords in self.GENRE_KEYWORDS.items():
            for keyword in keywords:
                if keyword in name_lower:
                    genres.append(genre)
                    break

        return genres

    def _extract_tags(self, name: str) -> List[str]:
        """Extrae tags del nombre del archivo"""
        import re

        tags = []
        name_lower = name.lower()

        # Palabras comunes como tags
        common_tags = [
            'dry', 'wet', 'processed', 'raw', 'analog', 'digital',
            'vintage', 'modern', 'punchy', 'deep', 'bright', 'dark',
            'tight', 'loose', 'fat', 'thin', 'crisp', 'warm',
            'one shot', 'loop', 'sample', 'hit'
        ]

        for tag in common_tags:
            if tag in name_lower:
                tags.append(tag.replace(' ', '_'))

        # Extraer números como versiones
        numbers = re.findall(r'\d+', name)
        for num in numbers:
            if len(num) <= 2:  # Probablemente versión
                tags.append(f"v{num}")

        return list(set(tags))

    def search(self,
               query: str = "",
               category: str = "",
               subcategory: str = "",
               sample_type: str = "",
               key: str = "",
               bpm: Optional[float] = None,
               bpm_tolerance: int = 5,
               genres: List[str] = None,
               tags: List[str] = None,
               min_rating: int = 0,
               favorites_only: bool = False,
               limit: int = 50,
               sort_by: str = "name") -> List[Sample]:
        """
        Búsqueda avanzada de samples con múltiples filtros.

        Args:
            query: Búsqueda por nombre
            category: Categoría principal
            subcategory: Subcategoría
            sample_type: Tipo específico
            key: Tonalidad musical
            bpm: BPM objetivo
            bpm_tolerance: Tolerancia de BPM (+/-)
            genres: Lista de géneros
            tags: Lista de tags
            min_rating: Rating mínimo
            favorites_only: Solo favoritos
            limit: Límite de resultados
            sort_by: Campo para ordenar

        Returns:
            Lista de samples que coinciden
        """
        with self._lock:
            results = []
            query_lower = query.lower()

            for sample in self.samples.values():
                # Filtro por query (nombre)
                query_haystack = " ".join([
                    sample.name,
                    sample.path,
                    " ".join(sample.tags),
                    " ".join(sample.genres),
                    sample.category,
                    sample.subcategory,
                    sample.sample_type,
                ]).lower()
                if query and query_lower not in query_haystack:
                    continue

                # Filtros de categoría
                if category and sample.category != category.lower():
                    continue
                if subcategory and sample.subcategory != subcategory.lower():
                    continue
                if sample_type and sample.sample_type != sample_type.lower():
                    continue

                # Filtro por key
                if key:
                    sample_key = (sample.key or "").lower()
                    if sample_key != key.lower():
                        # Intentar key compatible (mismo root)
                        if not sample_key.startswith(key.lower().rstrip('m')):
                            continue

                # Filtro por BPM
                if bpm is not None and sample.bpm:
                    if abs(sample.bpm - bpm) > bpm_tolerance:
                        continue

                # Filtro por géneros
                if genres:
                    sample_genres = [g.lower() for g in sample.genres]
                    if not any(g.lower() in sample_genres for g in genres):
                        continue

                # Filtro por tags
                if tags:
                    sample_tags = [t.lower() for t in sample.tags]
                    if not any(t.lower() in sample_tags for t in tags):
                        continue

                # Filtro por rating
                if min_rating > 0 and sample.rating < min_rating:
                    continue

                # Filtro de favoritos
                if favorites_only and sample.rating < 4:
                    continue

                results.append(sample)

            # Ordenar resultados
            if sort_by == "name":
                results.sort(key=lambda s: s.name.lower())
            elif sort_by == "bpm":
                results.sort(key=lambda s: s.bpm or 0)
            elif sort_by == "rating":
                results.sort(key=lambda s: s.rating, reverse=True)
            elif sort_by == "date_added":
                results.sort(key=lambda s: s.date_added, reverse=True)

            return results[:limit]

    def get_by_id(self, sample_id: str) -> Optional[Sample]:
        """Obtiene un sample por su ID"""
        with self._lock:
            return self.samples.get(sample_id)

    def get_by_path(self, file_path: str) -> Optional[Sample]:
        """Obtiene un sample por su ruta"""
        sample_id = self._generate_id(file_path)
        return self.get_by_id(sample_id)

    def get_random(self, category: str = "", limit: int = 1) -> List[Sample]:
        """Obtiene samples aleatorios"""
        import random

        with self._lock:
            samples = list(self.samples.values())

            if category:
                samples = [s for s in samples if s.category == category]

            if not samples:
                return []

            return random.sample(samples, min(limit, len(samples)))

    def get_pack_for_genre(self, genre: str, key: str = "",
                           bpm: Optional[float] = None) -> Dict[str, List[Sample]]:
        """
        Obtiene un pack completo de samples para un género específico.

        Returns:
            Dict con samples organizados por tipo
        """
        pack = {
            'kicks': [],
            'snares': [],
            'claps': [],
            'hats': [],
            'percussion': [],
            'bass': [],
            'synths': [],
            'fx': [],
        }

        # Buscar samples por tipo
        type_mapping = {
            'kicks': ['kick'],
            'snares': ['snare'],
            'claps': ['clap'],
            'hats': ['hat', 'hat_closed', 'hat_open'],
            'percussion': ['perc', 'shaker', 'tom', 'cymbal'],
            'bass': ['bass', 'sub', 'bassline', 'acid'],
            'synths': ['lead', 'pad', 'pluck', 'chord'],
            'fx': ['fx', 'hit', 'noise'],
        }

        for pack_category, sample_types in type_mapping.items():
            for sample_type in sample_types:
                samples = self.search(
                    sample_type=sample_type,
                    key=key,
                    bpm=bpm,
                    genres=[genre] if genre else None,
                    limit=5
                )

                if samples:
                    pack[pack_category].extend(samples)

        return pack

    def update_sample(self, sample_id: str, **kwargs) -> bool:
        """
        Actualiza metadatos de un sample.

        Args:
            sample_id: ID del sample
            **kwargs: Campos a actualizar
        """
        with self._lock:
            if sample_id not in self.samples:
                return False

            sample = self.samples[sample_id]

            # Campos permitidos para actualización
            allowed_fields = {
                'rating', 'tags', 'genres', 'mood', 'energy',
                'key', 'bpm', 'play_count'
            }

            for field, value in kwargs.items():
                if field in allowed_fields and hasattr(sample, field):
                    setattr(sample, field, value)

            sample.date_modified = datetime.now().isoformat()
            self._index_dirty = True

            return True

    def rate_sample(self, sample_id: str, rating: int) -> bool:
        """Califica un sample (1-5 estrellas)"""
        if 0 <= rating <= 5:
            return self.update_sample(sample_id, rating=rating)
        return False

    def increment_play_count(self, sample_id: str) -> bool:
        """Incrementa el contador de reproducciones"""
        sample = self.get_by_id(sample_id)
        if sample:
            return self.update_sample(sample_id, play_count=sample.play_count + 1)
        return False

    def delete_sample(self, sample_id: str, delete_file: bool = False) -> bool:
        """
        Elimina un sample del índice.

        Args:
            sample_id: ID del sample
            delete_file: Si True, también elimina el archivo físico
        """
        with self._lock:
            if sample_id not in self.samples:
                return False

            sample = self.samples[sample_id]

            if delete_file:
                try:
                    Path(sample.path).unlink()
                except Exception as e:
                    logger.error(f"Error eliminando archivo: {e}")
                    return False

            del self.samples[sample_id]
            self._index_dirty = True
            self._update_stats()

            return True

    def refresh(self, analyze_audio: bool = False) -> Dict[str, Any]:
        """Refresca el índice completo"""
        logger.info("Refrescando índice de samples...")

        # Guardar IDs actuales para detectar eliminados
        current_paths = {s.path for s in self.samples.values()}

        # Re-escanear
        stats = self.scan_directory(analyze_audio=analyze_audio)

        # Detectar archivos eliminados
        new_paths = {s.path for s in self.samples.values()}
        removed = current_paths - new_paths

        for path in removed:
            sample_id = self._generate_id(path)
            if sample_id in self.samples:
                del self.samples[sample_id]
                stats['removed'] = stats.get('removed', 0) + 1

        self._save_index()
        return stats

    def get_stats(self) -> Dict[str, Any]:
        """Obtiene estadísticas de la librería"""
        with self._lock:
            return {
                'total_samples': len(self.samples),
                'total_size': sum(s.file_size for s in self.samples.values()),
                'by_category': dict(self.stats['by_category']),
                'by_key': dict(self.stats['by_key']),
                'by_bpm_range': dict(self.stats['by_bpm_range']),
                'last_scan': self.stats['last_scan'],
            }

    def export_library(self, output_path: str, format: str = "json") -> str:
        """
        Exporta la librería a un archivo.

        Args:
            output_path: Ruta del archivo de salida
            format: 'json' o 'csv'

        Returns:
            Ruta del archivo exportado
        """
        output = Path(output_path)

        with self._lock:
            if format == "json":
                data = {
                    'export_date': datetime.now().isoformat(),
                    'stats': self.get_stats(),
                    'samples': [s.to_dict() for s in self.samples.values()]
                }
                with open(output, 'w', encoding='utf-8') as f:
                    json.dump(data, f, indent=2, ensure_ascii=False)

            elif format == "csv":
                import csv
                with open(output, 'w', newline='', encoding='utf-8') as f:
                    if self.samples:
                        writer = csv.DictWriter(f, fieldnames=self.samples[list(self.samples.keys())[0]].to_dict().keys())
                        writer.writeheader()
                        for sample in self.samples.values():
                            writer.writerow(sample.to_dict())

        return str(output)

    def import_library(self, input_path: str, merge: bool = True) -> Dict[str, int]:
        """
        Importa una librería desde un archivo JSON.

        Args:
            input_path: Ruta del archivo a importar
            merge: Si True, mezcla con la librería existente

        Returns:
            Estadísticas de la importación
        """
        with open(input_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        imported_samples = data.get('samples', [])

        with self._lock:
            if not merge:
                self.samples.clear()

            added = 0
            updated = 0

            for sample_data in imported_samples:
                try:
                    sample = Sample.from_dict(sample_data)
                    if sample.id in self.samples:
                        updated += 1
                    else:
                        added += 1
                    self.samples[sample.id] = sample
                except Exception as e:
                    logger.error(f"Error importando sample: {e}")

            self._index_dirty = True
            self._update_stats()
            self._save_index()

        return {'added': added, 'updated': updated}

    def _update_stats(self):
        """Actualiza las estadísticas de la librería"""
        self.stats['total_samples'] = len(self.samples)
        self.stats['total_size'] = sum(s.file_size for s in self.samples.values())

        # Resetear contadores
        self.stats['by_category'] = defaultdict(int)
        self.stats['by_key'] = defaultdict(int)
        self.stats['by_bpm_range'] = defaultdict(int)

        for sample in self.samples.values():
            self.stats['by_category'][sample.category] += 1

            if sample.key:
                self.stats['by_key'][sample.key] += 1

            if sample.bpm:
                if sample.bpm < 100:
                    self.stats['by_bpm_range']['slow (<100)'] += 1
                elif sample.bpm < 128:
                    self.stats['by_bpm_range']['mid (100-128)'] += 1
                elif sample.bpm < 140:
                    self.stats['by_bpm_range']['fast (128-140)'] += 1
                else:
                    self.stats['by_bpm_range']['very fast (>140)'] += 1

    def _load_index(self):
        """Carga el índice desde disco"""
        if not self.index_file.exists():
            logger.info("No existe índice previo, iniciando librería vacía")
            return

        try:
            with open(self.index_file, 'r', encoding='utf-8') as f:
                data = json.load(f)

            for sample_data in data.get('samples', []):
                try:
                    sample = Sample.from_dict(sample_data)
                    self.samples[sample.id] = sample
                except Exception as e:
                    logger.warning(f"Error cargando sample: {e}")

            self.stats = data.get('stats', self.stats)
            logger.info(f"Índice cargado: {len(self.samples)} samples")

        except Exception as e:
            logger.error(f"Error cargando índice: {e}")

    def _save_index(self):
        """Guarda el índice a disco"""
        if not self._index_dirty:
            return

        try:
            data = {
                'version': 1,
                'saved_at': datetime.now().isoformat(),
                'stats': self.get_stats(),
                'samples': [s.to_dict() for s in self.samples.values()]
            }

            # Guardar a archivo temporal primero
            temp_file = self.index_file.with_suffix('.tmp')
            with open(temp_file, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2, ensure_ascii=False)

            # Renombrar atómicamente
            temp_file.replace(self.index_file)

            self._index_dirty = False
            logger.info(f"Índice guardado: {len(self.samples)} samples")

        except Exception as e:
            logger.error(f"Error guardando índice: {e}")

    def save(self):
        """Fuerza el guardado del índice"""
        self._index_dirty = True
        self._save_index()


# Instancia global
_manager: Optional[SampleManager] = None


def get_manager(base_dir: Optional[str] = None) -> SampleManager:
    """Obtiene la instancia global del gestor"""
    global _manager
    resolved_base_dir = str(Path(base_dir).resolve()) if base_dir else str(DEFAULT_SAMPLE_MANAGER_DIR.resolve())
    current_base_dir = str(getattr(_manager, "base_dir", "") or "")
    if _manager is None or current_base_dir.lower() != resolved_base_dir.lower():
        if base_dir is None:
            base_dir = resolved_base_dir
        _manager = SampleManager(base_dir)
    return _manager


# Funciones de conveniencia
def scan_samples(directory: str, analyze_audio: bool = False) -> Dict[str, Any]:
    """Escanear directorio de samples"""
    manager = get_manager(directory)
    return manager.scan_directory(analyze_audio=analyze_audio)


def find_samples(query: str = "", **kwargs) -> List[Dict[str, Any]]:
    """Buscar samples"""
    manager = get_manager()
    samples = manager.search(query=query, **kwargs)
    return [s.to_dict() for s in samples]


def get_sample_pack(genre: str, key: str = "", bpm: Optional[float] = None) -> Dict[str, List[Dict]]:
    """Obtener pack de samples para un género"""
    manager = get_manager()
    pack = manager.get_pack_for_genre(genre, key, bpm)
    return {k: [s.to_dict() for s in v] for k, v in pack.items()}


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

    logging.basicConfig(level=logging.INFO)

    if len(sys.argv) < 2:
        print("Uso: python sample_manager.py <directorio> [comando]")
        print("\nComandos:")
        print("  scan      - Escanear directorio")
        print("  stats     - Mostrar estadísticas")
        print("  search    - Buscar samples")
        sys.exit(1)

    directory = sys.argv[1]
    command = sys.argv[2] if len(sys.argv) > 2 else "scan"

    manager = SampleManager(directory)

    if command == "scan":
        print(f"\nEscaneando: {directory}")
        print("=" * 50)

        def progress(current, total, filename):
            pct = (current / total) * 100
            print(f"\r[{pct:5.1f}%] {filename[:50]:<50}", end="", flush=True)

        stats = manager.scan_directory(progress_callback=progress)
        print("\n")
        print(f"Procesados: {stats['processed']}")
        print(f"Agregados: {stats['added']}")
        print(f"Actualizados: {stats['updated']}")
        print(f"Errores: {stats['errors']}")
        print(f"Total en librería: {stats['total_samples']}")

    elif command == "stats":
        stats = manager.get_stats()
        print("\nEstadísticas de la librería:")
        print("=" * 50)
        print(f"Total samples: {stats['total_samples']}")
        print(f"Tamaño total: {stats['total_size'] / (1024**2):.1f} MB")
        print(f"Último escaneo: {stats['last_scan']}")
        print("\nPor categoría:")
        for cat, count in sorted(stats['by_category'].items()):
            print(f"  {cat}: {count}")
        print("\nPor key:")
        for key, count in sorted(stats['by_key'].items()):
            print(f"  {key}: {count}")

    elif command == "search":
        query = sys.argv[3] if len(sys.argv) > 3 else ""
        print(f"\nBuscando: '{query}'")
        print("=" * 50)

        results = manager.search(query=query, limit=20)
        for s in results:
            print(f"\n{s.name}")
            print(f"  Categoría: {s.category}/{s.subcategory}")
            print(f"  Key: {s.key or 'N/A'} | BPM: {s.bpm or 'N/A'}")
            print(f"  Path: {s.path}")
