"""
midi_preset_indexer.py - Indexación de MIDI y presets de instrumentos

Proporciona:
- Escaneo de archivos MIDI (.mid, .midi) y presets (.fst)
- Mapeo automático a familias de instrumentos (piano, keys, pad, pluck, etc.)
- Indexación por pack y categoría
- Metadatos extraíbles de nombres de archivo
- Integración con reference_listener.py
"""

import json
import hashlib
import logging
import os
import re
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

logger = logging.getLogger("MIDIPresetIndexer")

# Directorio de configuración de usuario
USER_CONFIG_DIR = Path.home() / ".abletonmcp_ai"
USER_CONFIG_DIR.mkdir(exist_ok=True)

DEFAULT_INDEX_PATH = USER_CONFIG_DIR / "midi_preset_index.json"


# Mapeo de carpetas/nombres a familias de instrumentos
FAMILY_MAPPING = {
    'Piano': ['piano', 'keys', 'rhodes', 'epiano', 'grand piano', 'steinway', 
              'attack piano', 'ice piano', 'keyzone'],
    'Keys': ['keys', 'keyboard', 'electric piano', 'wurlitzer', 'clavinet'],
    'Guitar': ['guitar', 'acoustic', 'electric', 'spanish', 'nylon'],
    'Pad': ['pad', 'atmosphere', 'strings', 'ambient pad', 'space pad', 
            'deep space', 'analog pad', 'peaceful pad', 'transcendence'],
    'Pluck': ['pluck', 'bell', 'marimba', 'glockenspiel', 'arp', 'arpeggio',
              'alise pluck', 'bell memories', 'spark', 'velo kalimba'],
    'Lead': ['lead', 'synth lead', 'solo', 'divanity lead', 'electrolead',
             'bell lead', 'ocaripan', 'square rez', 'espress lead'],
    'Bass': ['bass', 'sub', 'subbass', '808', 'electrax bass'],
    'FX': ['fx', 'effect', 'riser', 'sweep', 'noise', 'impact'],
    'Vocal': ['vocal', 'vox', 'voice', 'choir', 'cyber choir'],
    'Drum': ['drum', 'kick', 'snare', 'hat', 'perc', 'dembow'],
    'Chord': ['chord', 'chords', 'progression', 'harmony'],
    'Arp': ['arp', 'arpeggio', 'arpelesta'],
    'Organ': ['organ', 'hammond', 'farfisa'],
    'Brass': ['brass', 'trumpet', 'sax', 'horn'],
    'String': ['string', 'violin', 'cello', 'ensemble'],
    'Percussion': ['percussion', 'conga', 'bongo', 'timbale'],
}

# Mapeo de sintetizadores/plugin a categorías
SYNTH_PLUGIN_MAPPING = {
    'diva': 'analog',
    'nexus': 'rompler',
    'serum': 'wavetable',
    'spire': 'virtual_analog',
    'ana 2': 'virtual_analog',
    'electrax': 'rompler',
    'hive': 'virtual_analog',
    'purity': 'rompler',
    'triton': 'workstation',
    'gms': 'virtual_analog',
    'iota mini': 'free_plugin',
    'poizone': 'free_plugin',
    'keyzone classic': 'piano_plugin',
    '3x osc': 'basic_synth',
    'toxic biohazard': 'fm_synth',
}


def _json_safe(value: Any) -> Any:
    """Convierte valores a formatos JSON-safe"""
    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 MIDIFile:
    """Representa un archivo MIDI en la librería"""
    id: str
    name: str
    path: str
    folder: str  # Carpeta contenedora
    pack: str  # Pack/kit al que pertenece
    type: str = "midi"
    
    # Metadatos musicales extraídos del nombre
    key: Optional[str] = None
    bpm: Optional[float] = None
    instrument_family: str = "Unknown"
    pattern_type: str = ""  # chord, arp, melody, drum, etc.
    
    # Información del archivo
    file_size: int = 0
    date_added: str = field(default_factory=lambda: datetime.now().isoformat())
    date_modified: str = field(default_factory=lambda: datetime.now().isoformat())
    
    def to_dict(self) -> Dict[str, Any]:
        """Convierte a diccionario"""
        return _json_safe(asdict(self))
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'MIDIFile':
        """Crea desde diccionario"""
        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)


@dataclass
class PresetFile:
    """Representa un archivo de preset (.fst) en la librería"""
    id: str
    name: str
    path: str
    folder: str
    pack: str
    type: str = "preset"
    
    # Información del sintetizador/plugin
    synth_plugin: str = ""  # diva, nexus, serum, etc.
    synth_category: str = ""  # analog, rompler, wavetable, etc.
    
    # Familia de instrumento
    instrument_family: str = "Unknown"
    
    # Metadatos
    file_size: int = 0
    date_added: str = field(default_factory=lambda: datetime.now().isoformat())
    date_modified: str = field(default_factory=lambda: datetime.now().isoformat())
    
    def to_dict(self) -> Dict[str, Any]:
        """Convierte a diccionario"""
        return _json_safe(asdict(self))
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'PresetFile':
        """Crea desde diccionario"""
        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)


class MIDIPresetIndexer:
    """
    Indexador de archivos MIDI y presets.
    
    Características:
    - Escaneo recursivo de directorios
    - Clasificación automática por familia de instrumento
    - Detección de key y BPM desde nombres de archivo
    - Indexación por pack/carpeta
    - Búsqueda avanzada por familia, tipo, pack
    - Persistencia en JSON
    """
    
    # Extensiones soportadas
    MIDI_EXTENSIONS = {'.mid', '.midi'}
    PRESET_EXTENSION = '.fst'
    
    # Carpetas a ignorar (audio loops)
    IGNORED_SEGMENTS = {'audio', 'loops', 'wav', 'aif', 'mp3', 
                        '__pycache__', '.sample_cache', 'documentation'}
    
    def __init__(self, library_path: Optional[str] = None, 
                 index_path: Optional[str] = None):
        """
        Inicializa el indexador.
        
        Args:
            library_path: Directorio raíz de la librería (default: libreria/reggaeton)
            index_path: Ruta para guardar el índice (default: ~/.abletonmcp_ai/midi_preset_index.json)
        """
        if library_path:
            self.library_path = Path(library_path)
        else:
            # Default path desde ProgramData
            default_lib = Path("C:/ProgramData/Ableton/Live 12 Suite/Resources/MIDI Remote Scripts/libreria/reggaeton")
            self.library_path = default_lib if default_lib.exists() else None
        
        self.index_path = Path(index_path) if index_path else DEFAULT_INDEX_PATH
        
        # Almacenamiento
        self.midi_files: Dict[str, MIDIFile] = {}
        self.preset_files: Dict[str, PresetFile] = {}
        
        # Índices organizados
        self.by_family: Dict[str, List[str]] = defaultdict(list)  # family -> list of ids
        self.by_pack: Dict[str, Dict[str, List[str]]] = defaultdict(lambda: {'midi': [], 'presets': []})
        self.by_type: Dict[str, List[str]] = {'midi': [], 'preset': []}
        
        # Estadísticas
        self.stats = {
            'total_midi': 0,
            'total_presets': 0,
            'by_family': defaultdict(int),
            'by_pack': defaultdict(lambda: {'midi': 0, 'presets': 0}),
            'last_scan': None,
        }
        
        # Cargar índice existente
        self._load_index()
    
    def _generate_id(self, file_path: str) -> str:
        """Genera ID único basado en ruta"""
        return hashlib.md5(file_path.encode()).hexdigest()[:16]
    
    def _should_ignore_path(self, file_path: Path) -> bool:
        """Determina si una ruta debe ignorarse"""
        path_str = str(file_path).lower()
        return any(segment.lower() in path_str for segment in self.IGNORED_SEGMENTS)
    
    def _extract_pack_name(self, file_path: Path) -> str:
        """Extrae el nombre del pack desde la ruta"""
        try:
            rel_path = file_path.relative_to(self.library_path)
            # El primer componente es el pack
            return str(rel_path.parts[0]) if rel_path.parts else "Unknown"
        except ValueError:
            return "Unknown"
    
    def _extract_folder_name(self, file_path: Path) -> str:
        """Extrae el nombre de la carpeta contenedora"""
        return file_path.parent.name
    
    def _map_to_family(self, folder_name: str, file_name: str) -> str:
        """
        Mapea carpeta/nombre de archivo a familia de instrumento.
        
        Args:
            folder_name: Nombre de la carpeta contenedora
            file_name: Nombre del archivo
            
        Returns:
            Nombre de la familia (Piano, Pad, Lead, etc.)
        """
        context = (folder_name + " " + file_name).lower()
        
        # Buscar coincidencias en el mapeo
        for family, keywords in FAMILY_MAPPING.items():
            if any(kw in context for kw in keywords):
                return family
        
        # Detección específica por palabras clave en nombre
        if any(x in context for x in ['chord', 'acorde', 'progresion']):
            return 'Chord'
        if any(x in context for x in ['arp', 'arpeggio', 'arpegiado']):
            return 'Arp'
        if any(x in context for x in ['melody', 'melodia', 'lead']):
            return 'Lead'
        if any(x in context for x in ['drum', 'bateria', 'perc']):
            return 'Drum'
        if any(x in context for x in ['bass', 'bajo', 'sub']):
            return 'Bass'
        
        return 'Unknown'
    
    def _extract_synth_plugin(self, file_name: str) -> Tuple[str, str]:
        """
        Extrae el sintetizador/plugin desde el nombre del preset.
        
        Returns:
            Tuple (plugin_name, category)
        """
        name_lower = file_name.lower()
        
        for plugin, category in SYNTH_PLUGIN_MAPPING.items():
            if plugin in name_lower:
                return plugin, category
        
        return "", ""
    
    def _extract_key_from_name(self, name: str) -> Optional[str]:
        """Extrae la tonalidad del nombre de archivo"""
        patterns = [
            r'[_\s\-]([A-G][#b]?(?:m|min|minor|maj|major)?)[_\s\-]',
            r'\bin\s+([A-G][#b]?(?:m|min|minor|maj|major)?)\b',
            r'Key[_\s]?([A-G][#b]?(?:m|min|minor|maj|major)?)',
        ]
        
        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', '').replace('maj', '')
                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 BPM del nombre de archivo"""
        patterns = [
            r'[_\s\-](\d{2,3})\s*BPM',
            r'(\d{2,3})bpm',
            r'[_\s\-](\d{2,3})[_\s\-]',
        ]
        
        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_pattern_type(self, folder_name: str, file_name: str) -> str:
        """Extrae el tipo de patrón MIDI"""
        context = (folder_name + " " + file_name).lower()
        
        if any(x in context for x in ['chord', 'chords', 'acorde', 'progresion', 'harmony']):
            return 'chord'
        if any(x in context for x in ['arp', 'arpeggio', 'arpegiado', 'arpelesta']):
            return 'arp'
        if any(x in context for x in ['melody', 'melodia', 'theme', 'motif']):
            return 'melody'
        if any(x in context for x in ['drum', 'bateria', 'beat', 'perc']):
            return 'drum'
        if any(x in context for x in ['bass', 'bajo', 'bassline']):
            return 'bass'
        if any(x in context for x in ['pad', 'ambient']):
            return 'pad'
        
        return 'unknown'
    
    def scan_library(self, library_path: Optional[str] = None,
                     progress_callback: Optional[Callable[[int, int, str], None]] = None) -> Dict[str, Any]:
        """
        Escanear la librería completa en busca de MIDI y presets.
        
        Args:
            library_path: Directorio a escanear (default: self.library_path)
            progress_callback: Función llamada con (procesados, total, archivo_actual)
            
        Returns:
            Estadísticas del escaneo
        """
        scan_dir = Path(library_path) if library_path else self.library_path
        
        if not scan_dir or not scan_dir.exists():
            raise FileNotFoundError(f"Directorio no encontrado: {scan_dir}")
        
        logger.info(f"Escaneando librería MIDI/presets: {scan_dir}")
        
        # Encontrar todos los archivos MIDI y presets
        all_files = []
        for root, dirs, files in os.walk(scan_dir):
            # Filtrar directorios ignorados
            dirs[:] = [d for d in dirs if not self._should_ignore_path(Path(root) / d)]
            
            for file in files:
                file_lower = file.lower()
                if file_lower.endswith(('.mid', '.midi', '.fst')):
                    all_files.append(Path(root) / file)
        
        total = len(all_files)
        processed = 0
        midi_added = 0
        presets_added = 0
        errors = 0
        
        logger.info(f"Encontrados {total} archivos MIDI/preset")
        
        # Procesar archivos
        for file_path in all_files:
            processed += 1
            
            if progress_callback:
                progress_callback(processed, total, str(file_path.name))
            
            try:
                result = self._process_file(file_path)
                if result == 'midi_added':
                    midi_added += 1
                elif result == 'preset_added':
                    presets_added += 1
            except Exception as e:
                logger.error(f"Error procesando {file_path}: {e}")
                errors += 1
        
        # Actualizar estadísticas e índices
        self._update_indices()
        self._update_stats()
        self._save_index()
        
        self.stats['last_scan'] = datetime.now().isoformat()
        
        return {
            'processed': processed,
            'midi_added': midi_added,
            'presets_added': presets_added,
            'errors': errors,
            'total_midi': len(self.midi_files),
            'total_presets': len(self.preset_files),
        }
    
    def _process_file(self, file_path: Path) -> str:
        """Procesa un archivo individual. Retorna tipo de acción."""
        file_id = self._generate_id(str(file_path))
        
        # Verificar si ya existe
        if file_id in self.midi_files or file_id in self.preset_files:
            return 'unchanged'
        
        folder_name = self._extract_folder_name(file_path)
        pack_name = self._extract_pack_name(file_path)
        file_stat = file_path.stat()
        
        if file_path.suffix.lower() in self.MIDI_EXTENSIONS:
            # Procesar archivo MIDI
            family = self._map_to_family(folder_name, file_path.stem)
            key = self._extract_key_from_name(file_path.stem)
            bpm = self._extract_bpm_from_name(file_path.stem)
            pattern_type = self._extract_pattern_type(folder_name, file_path.stem)
            
            midi_file = MIDIFile(
                id=file_id,
                name=file_path.stem,
                path=str(file_path),
                folder=folder_name,
                pack=pack_name,
                type='midi',
                key=key,
                bpm=bpm,
                instrument_family=family,
                pattern_type=pattern_type,
                file_size=file_stat.st_size,
                date_modified=datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
            )
            
            self.midi_files[file_id] = midi_file
            return 'midi_added'
            
        elif file_path.suffix.lower() == self.PRESET_EXTENSION:
            # Procesar archivo preset
            family = self._map_to_family(folder_name, file_path.stem)
            synth_plugin, synth_category = self._extract_synth_plugin(file_path.stem)
            
            preset_file = PresetFile(
                id=file_id,
                name=file_path.stem,
                path=str(file_path),
                folder=folder_name,
                pack=pack_name,
                type='preset',
                synth_plugin=synth_plugin,
                synth_category=synth_category,
                instrument_family=family,
                file_size=file_stat.st_size,
                date_modified=datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
            )
            
            self.preset_files[file_id] = preset_file
            return 'preset_added'
        
        return 'unknown'
    
    def _update_indices(self):
        """Actualiza los índices organizados"""
        # Limpiar índices
        self.by_family = defaultdict(list)
        self.by_pack = defaultdict(lambda: {'midi': [], 'presets': []})
        self.by_type = {'midi': [], 'preset': []}
        
        # Indexar archivos MIDI
        for file_id, midi in self.midi_files.items():
            self.by_family[midi.instrument_family].append(file_id)
            self.by_pack[midi.pack]['midi'].append(file_id)
            self.by_type['midi'].append(file_id)
        
        # Indexar presets
        for file_id, preset in self.preset_files.items():
            self.by_family[preset.instrument_family].append(file_id)
            self.by_pack[preset.pack]['presets'].append(file_id)
            self.by_type['preset'].append(file_id)
    
    def _update_stats(self):
        """Actualiza las estadísticas"""
        self.stats['total_midi'] = len(self.midi_files)
        self.stats['total_presets'] = len(self.preset_files)
        
        # Contar por familia
        self.stats['by_family'] = defaultdict(int)
        for midi in self.midi_files.values():
            self.stats['by_family'][midi.instrument_family] += 1
        for preset in self.preset_files.values():
            self.stats['by_family'][preset.instrument_family] += 1
        
        # Contar por pack
        self.stats['by_pack'] = defaultdict(lambda: {'midi': 0, 'presets': 0})
        for midi in self.midi_files.values():
            self.stats['by_pack'][midi.pack]['midi'] += 1
        for preset in self.preset_files.values():
            self.stats['by_pack'][preset.pack]['presets'] += 1
    
    def search(self,
               query: str = "",
               family: str = "",
               file_type: str = "",  # 'midi' o 'preset'
               pack: str = "",
               key: str = "",
               bpm: Optional[float] = None,
               bpm_tolerance: int = 5,
               synth_plugin: str = "",
               limit: int = 50) -> Dict[str, List[Dict[str, Any]]]:
        """
        Búsqueda avanzada de MIDI y presets.
        
        Args:
            query: Búsqueda por nombre
            family: Familia de instrumento (Piano, Pad, Lead, etc.)
            file_type: 'midi' o 'preset'
            pack: Nombre del pack
            key: Tonalidad musical
            bpm: BPM objetivo
            bpm_tolerance: Tolerancia de BPM
            synth_plugin: Plugin específico (para presets)
            limit: Límite de resultados
            
        Returns:
            Dict con 'midi' y 'presets' listados
        """
        results = {'midi': [], 'presets': []}
        query_lower = query.lower()
        
        # Filtrar MIDI files
        if not file_type or file_type == 'midi':
            for midi in self.midi_files.values():
                # Filtro por query
                if query and query_lower not in (midi.name + midi.folder + midi.pack).lower():
                    continue
                
                # Filtro por familia
                if family and midi.instrument_family.lower() != family.lower():
                    continue
                
                # Filtro por pack
                if pack and pack.lower() not in midi.pack.lower():
                    continue
                
                # Filtro por key
                if key and (midi.key or "").lower() != key.lower():
                    continue
                
                # Filtro por BPM
                if bpm is not None and midi.bpm:
                    if abs(midi.bpm - bpm) > bpm_tolerance:
                        continue
                
                results['midi'].append(midi.to_dict())
                if len(results['midi']) >= limit:
                    break
        
        # Filtrar presets
        if not file_type or file_type == 'preset':
            for preset in self.preset_files.values():
                # Filtro por query
                if query and query_lower not in (preset.name + preset.folder + preset.pack).lower():
                    continue
                
                # Filtro por familia
                if family and preset.instrument_family.lower() != family.lower():
                    continue
                
                # Filtro por pack
                if pack and pack.lower() not in preset.pack.lower():
                    continue
                
                # Filtro por synth plugin
                if synth_plugin and synth_plugin.lower() not in preset.synth_plugin.lower():
                    continue
                
                results['presets'].append(preset.to_dict())
                if len(results['presets']) >= limit:
                    break
        
        return results
    
    def get_by_family(self, family: str) -> Dict[str, List[Dict[str, Any]]]:
        """Obtiene todos los archivos de una familia de instrumento"""
        results = {'midi': [], 'presets': []}
        
        for file_id in self.by_family.get(family, []):
            if file_id in self.midi_files:
                results['midi'].append(self.midi_files[file_id].to_dict())
            elif file_id in self.preset_files:
                results['presets'].append(self.preset_files[file_id].to_dict())
        
        return results
    
    def get_by_pack(self, pack: str) -> Dict[str, List[Dict[str, Any]]]:
        """Obtiene todos los archivos de un pack"""
        results = {'midi': [], 'presets': []}
        
        pack_data = self.by_pack.get(pack, {'midi': [], 'presets': []})
        
        for file_id in pack_data['midi']:
            if file_id in self.midi_files:
                results['midi'].append(self.midi_files[file_id].to_dict())
        
        for file_id in pack_data['presets']:
            if file_id in self.preset_files:
                results['presets'].append(self.preset_files[file_id].to_dict())
        
        return results
    
    def get_families(self) -> List[str]:
        """Retorna lista de familias disponibles"""
        return sorted(self.by_family.keys())
    
    def get_packs(self) -> List[str]:
        """Retorna lista de packs disponibles"""
        return sorted(self.by_pack.keys())
    
    def get_stats(self) -> Dict[str, Any]:
        """Obtiene estadísticas completas"""
        return {
            'total_midi': len(self.midi_files),
            'total_presets': len(self.preset_files),
            'by_family': dict(self.stats['by_family']),
            'by_pack': {k: dict(v) for k, v in self.stats['by_pack'].items()},
            'families_available': self.get_families(),
            'packs_available': self.get_packs(),
            'last_scan': self.stats['last_scan'],
            'index_location': str(self.index_path),
        }
    
    def _save_index(self):
        """Guarda el índice a disco"""
        try:
            data = {
                'version': 1,
                'saved_at': datetime.now().isoformat(),
                'library_path': str(self.library_path) if self.library_path else None,
                'stats': self.get_stats(),
                'midi_files': {k: v.to_dict() for k, v in self.midi_files.items()},
                'preset_files': {k: v.to_dict() for k, v in self.preset_files.items()},
                'by_family': dict(self.by_family),
                'by_pack': {k: dict(v) for k, v in self.by_pack.items()},
                'by_type': self.by_type,
            }
            
            # Guardar a archivo temporal primero
            temp_file = self.index_path.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_path)
            
            logger.info(f"Índice guardado: {len(self.midi_files)} MIDI, {len(self.preset_files)} presets")
            
        except Exception as e:
            logger.error(f"Error guardando índice: {e}")
    
    def _load_index(self):
        """Carga el índice desde disco"""
        if not self.index_path.exists():
            logger.info("No existe índice previo de MIDI/presets")
            return
        
        try:
            with open(self.index_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            # Cargar archivos MIDI
            for midi_data in data.get('midi_files', {}).values():
                try:
                    midi = MIDIFile.from_dict(midi_data)
                    self.midi_files[midi.id] = midi
                except Exception as e:
                    logger.warning(f"Error cargando MIDI: {e}")
            
            # Cargar presets
            for preset_data in data.get('preset_files', {}).values():
                try:
                    preset = PresetFile.from_dict(preset_data)
                    self.preset_files[preset.id] = preset
                except Exception as e:
                    logger.warning(f"Error cargando preset: {e}")
            
            # Restaurar índices
            self._update_indices()
            self._update_stats()
            
            logger.info(f"Índice cargado: {len(self.midi_files)} MIDI, {len(self.preset_files)} presets")
            
        except Exception as e:
            logger.error(f"Error cargando índice: {e}")
    
    def refresh(self) -> Dict[str, Any]:
        """Refresca el índice completo"""
        logger.info("Refrescando índice de MIDI/presets...")
        
        # Guardar IDs actuales
        current_paths = {m.path for m in self.midi_files.values()}
        current_paths.update({p.path for p in self.preset_files.values()})
        
        # Re-escanear
        stats = self.scan_library()
        
        # Detectar archivos eliminados
        new_midi_paths = {m.path for m in self.midi_files.values()}
        new_preset_paths = {p.path for p in self.preset_files.values()}
        new_all_paths = new_midi_paths | new_preset_paths
        
        removed = current_paths - new_all_paths
        
        for path in removed:
            file_id = self._generate_id(path)
            if file_id in self.midi_files:
                del self.midi_files[file_id]
                stats['removed'] = stats.get('removed', 0) + 1
            elif file_id in self.preset_files:
                del self.preset_files[file_id]
                stats['removed'] = stats.get('removed', 0) + 1
        
        self._update_indices()
        self._update_stats()
        self._save_index()
        
        return stats


# Instancia global
_indexer: Optional[MIDIPresetIndexer] = None


def get_indexer(library_path: Optional[str] = None) -> MIDIPresetIndexer:
    """Obtiene la instancia global del indexador"""
    global _indexer
    if _indexer is None:
        _indexer = MIDIPresetIndexer(library_path)
    return _indexer


def scan_midi_presets(library_path: Optional[str] = None) -> Dict[str, Any]:
    """Escanear librería de MIDI y presets"""
    indexer = get_indexer(library_path)
    return indexer.scan_library()


def search_midi_presets(query: str = "", **kwargs) -> Dict[str, List[Dict[str, Any]]]:
    """Buscar MIDI y presets"""
    indexer = get_indexer()
    return indexer.search(query=query, **kwargs)


def get_midi_preset_stats() -> Dict[str, Any]:
    """Obtener estadísticas de MIDI/presets"""
    indexer = get_indexer()
    return indexer.get_stats()


def query_by_family(family: str) -> Dict[str, List[Dict[str, Any]]]:
    """Consultar archivos por familia de instrumento"""
    indexer = get_indexer()
    return indexer.get_by_family(family)


def query_by_pack(pack: str) -> Dict[str, List[Dict[str, Any]]]:
    """Consultar archivos por pack"""
    indexer = get_indexer()
    return indexer.get_by_pack(pack)


# Testing
if __name__ == "__main__":
    import sys
    
    logging.basicConfig(level=logging.INFO)
    
    if len(sys.argv) < 2:
        print("Uso: python midi_preset_indexer.py <comando> [args]")
        print("\nComandos:")
        print("  scan [path]       - Escanear librería")
        print("  stats             - Mostrar estadísticas")
        print("  search <query>    - Buscar archivos")
        print("  family <name>     - Buscar por familia (Piano, Pad, Lead, etc.)")
        print("  pack <name>       - Buscar por pack")
        sys.exit(1)
    
    command = sys.argv[1]
    
    if command == "scan":
        library_path = sys.argv[2] if len(sys.argv) > 2 else None
        print(f"\nEscaneando librería MIDI/presets...")
        print("=" * 50)
        
        def progress(current, total, filename):
            pct = (current / total) * 100
            print(f"\r[{pct:5.1f}%] {filename[:50]:<50}", end="", flush=True)
        
        indexer = get_indexer(library_path)
        stats = indexer.scan_library(progress_callback=progress)
        print("\n")
        print(f"Procesados: {stats['processed']}")
        print(f"MIDI agregados: {stats['midi_added']}")
        print(f"Presets agregados: {stats['presets_added']}")
        print(f"Errores: {stats['errors']}")
        print(f"Total MIDI: {stats['total_midi']}")
        print(f"Total Presets: {stats['total_presets']}")
        print(f"\nÍndice guardado en: {indexer.index_path}")
    
    elif command == "stats":
        indexer = get_indexer()
        stats = indexer.get_stats()
        print("\nEstadísticas de MIDI/Presets:")
        print("=" * 50)
        print(f"Total MIDI: {stats['total_midi']}")
        print(f"Total Presets: {stats['total_presets']}")
        print(f"Último escaneo: {stats['last_scan']}")
        print(f"Ubicación del índice: {stats['index_location']}")
        print("\nPor familia:")
        for family, count in sorted(stats['by_family'].items()):
            print(f"  {family}: {count}")
        print("\nPacks disponibles:")
        for pack in sorted(stats['packs_available']):
            midi_count = stats['by_pack'].get(pack, {}).get('midi', 0)
            preset_count = stats['by_pack'].get(pack, {}).get('presets', 0)
            print(f"  {pack}: {midi_count} MIDI, {preset_count} presets")
    
    elif command == "search":
        query = sys.argv[2] if len(sys.argv) > 2 else ""
        print(f"\nBuscando: '{query}'")
        print("=" * 50)
        
        indexer = get_indexer()
        results = indexer.search(query=query, limit=20)
        
        if results['midi']:
            print(f"\nMIDI encontrados ({len(results['midi'])}):")
            for m in results['midi']:
                print(f"  {m['name']}")
                print(f"    Familia: {m['instrument_family']} | Pack: {m['pack']}")
                print(f"    Key: {m['key'] or 'N/A'} | BPM: {m['bpm'] or 'N/A'}")
        
        if results['presets']:
            print(f"\nPresets encontrados ({len(results['presets'])}):")
            for p in results['presets']:
                print(f"  {p['name']}")
                print(f"    Familia: {p['instrument_family']} | Plugin: {p['synth_plugin']}")
                print(f"    Pack: {p['pack']}")
    
    elif command == "family":
        if len(sys.argv) < 3:
            print("Error: Debes especificar una familia")
            print("Familias disponibles: Piano, Keys, Guitar, Pad, Pluck, Lead, Bass, FX, Vocal, Drum, Chord, Arp, Organ, Brass, String, Percussion")
            sys.exit(1)
        
        family = sys.argv[2]
        print(f"\nBuscando familia: '{family}'")
        print("=" * 50)
        
        indexer = get_indexer()
        results = indexer.get_by_family(family)
        
        if results['midi']:
            print(f"\nMIDI ({len(results['midi'])}):")
            for m in results['midi']:
                print(f"  - {m['name']} ({m['pack']})")
        
        if results['presets']:
            print(f"\nPresets ({len(results['presets'])}):")
            for p in results['presets']:
                print(f"  - {p['name']} ({p['synth_plugin'] or 'unknown synth'})")
        
        if not results['midi'] and not results['presets']:
            print("No se encontraron archivos en esta familia")
    
    elif command == "pack":
        if len(sys.argv) < 3:
            print("Error: Debes especificar un pack")
            sys.exit(1)
        
        pack = sys.argv[2]
        print(f"\nBuscando pack: '{pack}'")
        print("=" * 50)
        
        indexer = get_indexer()
        results = indexer.get_by_pack(pack)
        
        if results['midi']:
            print(f"\nMIDI ({len(results['midi'])}):")
            for m in results['midi'][:20]:
                print(f"  - {m['name']} ({m['instrument_family']})")
            if len(results['midi']) > 20:
                print(f"  ... y {len(results['midi']) - 20} más")
        
        if results['presets']:
            print(f"\nPresets ({len(results['presets'])}):")
            for p in results['presets'][:20]:
                print(f"  - {p['name']} ({p['instrument_family']})")
            if len(results['presets']) > 20:
                print(f"  ... y {len(results['presets']) - 20} más")
        
        if not results['midi'] and not results['presets']:
            print("No se encontraron archivos en este pack")
