"""
groove_extractor.py - Extractor de groove de loops dembow reales.

T115: Sistema de extracción de groove para mejorar patrones rítmicos.

Lee transitorios, densidad y acentos de loops dembow reales y los usa
para posicionar kicks, claps y hats con feel más humano y menos mecánico.
"""

import os
import json
import logging
from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass, asdict
import random

logger = logging.getLogger("GrooveExtractor")

# Paths
# Get project root (MIDI Remote Scripts directory)
SERVER_DIR = Path(__file__).resolve().parent
MCP_SERVER_DIR = SERVER_DIR  # MCP_Server
ABLETONMCP_AI_DIR = MCP_SERVER_DIR.parent  # AbletonMCP_AI
PACKAGE_DIR = ABLETONMCP_AI_DIR.parent  # AbletonMCP_AI (package)
SCRIPTS_ROOT = PACKAGE_DIR.parent  # MIDI Remote Scripts
REGGAETON_DIR = SCRIPTS_ROOT / "libreria" / "reggaeton"
GROOVE_CACHE_PATH = Path.home() / ".abletonmcp_ai" / "dembow_groove_templates.json"


@dataclass
class GrooveTemplate:
    """Template de groove extraído de un loop real."""
    source_file: str
    bpm: float
    
    # Posiciones normalizadas (0-4 beats, relativo al compás)
    kick_positions: List[float]
    snare_positions: List[float]  # clap/snare
    hat_positions: List[float]
    
    # Velocidades relativas (0.0 - 1.0)
    kick_velocities: List[float]
    snare_velocities: List[float]
    hat_velocities: List[float]
    
    # Timing variations in ms (desviaciones del grid)
    timing_variance_ms: float
    
    # Densidad del patrón
    density: float
    
    # Metadata
    style: str = "dembow"
    extracted_at: Optional[float] = None
    
    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "GrooveTemplate":
        return cls(**data)


class DembowGrooveExtractor:
    """
    Extrae y almacena templates de groove de loops dembow reales.
    Soporta múltiples directorios, escaneo recursivo y deduplicación.
    """
    
    # Directorios a escanear dentro de libreria/reggaeton
    SCAN_DIRS = ['drumloops', 'perc loop', 'oneshots']
    
    # Carpetas y archivos a ignorar (solo en raíz o archivos específicos)
    IGNORE_PATTERNS = [
        '.sample_cache', '.segment_rag', '.git',
        'temp', 'tmp', 'cache',  # Solo ignorar en contexto de archivos/carpetas de sistema
        'doc', 'docs', 'documentation',
        'trash', 'recycle', 'deleted',
        '.json', '.txt', '.md', '.doc', '.docx',
    ]
    
    # Carpetas de sistema a ignorar completamente
    IGNORED_FOLDERS = {
        '.sample_cache', '.segment_rag', '.git', 
        'trash', 'recycle', 'deleted', '__pycache__'
    }
    
    def __init__(self):
        self.templates: Dict[str, GrooveTemplate] = {}
        self._processed_hashes: set = set()  # Para deduplicación
        self._load_cache()
        
    def _should_ignore_path(self, path: Path) -> bool:
        """Determina si un archivo o directorio debe ser ignorado."""
        path_str = str(path).lower()
        name = path.name.lower()
        
        # Ignorar archivos ocultos (empiezan con .)
        if name.startswith('.'):
            return True
            
        # Ignorar carpetas de sistema específicas
        for folder in self.IGNORED_FOLDERS:
            if folder.lower() in path_str:
                return True
                
        # Ignorar archivos que no son wav
        if path.is_file() and not path.suffix.lower() == '.wav':
            return True
            
        return False
    
    def _compute_file_hash(self, file_path: Path) -> str:
        """Computa un hash simple basado en nombre, tamaño y fecha de modificación."""
        try:
            stat = file_path.stat()
            # Usar nombre, tamaño y mtime como identificador único
            hash_input = f"{file_path.name}:{stat.st_size}:{stat.st_mtime:.0f}"
            import hashlib
            return hashlib.md5(hash_input.encode()).hexdigest()[:16]
        except Exception:
            return file_path.name
    
    def _find_wav_files_recursive(self, base_dir: Path) -> List[Path]:
        """
        Encuentra todos los archivos .wav recursivamente, aplicando filtros.
        
        Args:
            base_dir: Directorio base para la búsqueda
            
        Returns:
            Lista de rutas a archivos .wav válidos
        """
        wav_files = []
        
        if not base_dir.exists():
            logger.warning(f"Directorio no existe: {base_dir}")
            return wav_files
        
        # Escaneo recursivo con rglob
        try:
            for wav_file in base_dir.rglob('*.wav'):
                # Verificar si debe ignorarse
                if self._should_ignore_path(wav_file):
                    continue
                    
                # Verificar que el archivo tiene tamaño válido
                try:
                    if wav_file.stat().st_size < 1024:  # Mínimo 1KB
                        logger.debug(f"Archivo muy pequeño, ignorando: {wav_file.name}")
                        continue
                except Exception:
                    continue
                    
                wav_files.append(wav_file)
                
        except Exception as e:
            logger.warning(f"Error escaneando {base_dir}: {e}")
            
        return wav_files
    
    def _get_drumloop_directories(self) -> List[Path]:
        """
        Obtiene la lista de directorios a escanear para drum loops.
        Busca en SCAN_DIRS dentro de libreria/reggaeton.
        """
        directories = []
        
        for scan_dir_name in self.SCAN_DIRS:
            scan_path = REGGAETON_DIR / scan_dir_name
            if scan_path.exists() and scan_path.is_dir():
                directories.append(scan_path)
                logger.info(f"Encontrado directorio de scan: {scan_path}")
            else:
                logger.debug(f"Directorio no encontrado: {scan_path}")
        
        # Siempre incluir drumloops si existe (fallback)
        drumloops_dir = REGGAETON_DIR / "drumloops"
        if drumloops_dir.exists() and drumloops_dir not in directories:
            directories.append(drumloops_dir)
            logger.info(f"Añadido drumloops fallback: {drumloops_dir}")
            
        return directories
        
    def _load_cache(self) -> None:
        """Carga templates cacheados desde disco."""
        try:
            if GROOVE_CACHE_PATH.exists():
                with open(GROOVE_CACHE_PATH, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                for key, template_dict in data.items():
                    self.templates[key] = GrooveTemplate.from_dict(template_dict)
                logger.info(f"✓ Groove cache cargado: {len(self.templates)} templates")
            else:
                logger.info("No hay groove cache previo")
        except Exception as e:
            logger.warning(f"⚠ Error cargando groove cache: {e}")
            self.templates = {}
    
    def _save_cache(self) -> None:
        """Guarda templates a disco."""
        try:
            GROOVE_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
            data = {k: v.to_dict() for k, v in self.templates.items()}
            with open(GROOVE_CACHE_PATH, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2)
            logger.debug(f"Groove cache guardado: {len(self.templates)} templates")
        except Exception as e:
            logger.warning(f"⚠ Error guardando groove cache: {e}")
    
    def scan_and_extract(self, force_reextract: bool = False) -> int:
        """
        Escanea loops dembow y extrae templates de groove.
        Soporta múltiples directorios y escaneo recursivo.
        
        Args:
            force_reextract: Si True, re-extrae aunque ya exista cache
            
        Returns:
            Número de templates extraídos
        """
        try:
            # Importar aquí para evitar dependencia circular
            from audio_analyzer import analyze_sample
        except ImportError:
            logger.error("No se pudo importar audio_analyzer")
            return 0
        
        # Obtener directorios a escanear
        scan_directories = self._get_drumloop_directories()
        
        if not scan_directories:
            logger.warning(f"No se encontraron directorios válidos en {REGGAETON_DIR}")
            return 0
        
        # Encontrar todos los archivos wav recursivamente
        all_wav_files = []
        for scan_dir in scan_directories:
            wav_files = self._find_wav_files_recursive(scan_dir)
            all_wav_files.extend(wav_files)
            logger.info(f"Encontrados {len(wav_files)} archivos en {scan_dir.name}")
        
        if not all_wav_files:
            logger.warning("No se encontraron archivos .wav válidos")
            return 0
        
        logger.info(f"Total de archivos a procesar: {len(all_wav_files)}")
        
        extracted_count = 0
        skipped_count = 0
        error_count = 0
        
        # Procesar cada archivo
        for wav_file in all_wav_files:
            file_key = str(wav_file)
            
            # Verificar deduplicación por hash
            file_hash = self._compute_file_hash(wav_file)
            if file_hash in self._processed_hashes and not force_reextract:
                skipped_count += 1
                continue
            
            # Saltar si ya existe en cache y no forzamos re-extracción
            if file_key in self.templates and not force_reextract:
                self._processed_hashes.add(file_hash)
                skipped_count += 1
                continue
            
            try:
                logger.info(f"Analizando {wav_file.name}...")
                analysis = analyze_sample(str(wav_file))
                
                groove_data = analysis.get('groove_template')
                if not groove_data:
                    logger.warning(f"  ⚠ No se pudo extraer groove de {wav_file.name}")
                    error_count += 1
                    continue
                
                # Validar que tiene suficientes transientes para un patrón útil
                total_hits = (
                    len(groove_data.get('kick_positions', [])) +
                    len(groove_data.get('snare_positions', [])) +
                    len(groove_data.get('hat_positions', []))
                )
                
                if total_hits < 3:
                    logger.warning(f"  ⚠ Patrón muy simple en {wav_file.name} ({total_hits} hits)")
                    # Aún así lo guardamos, pero con advertencia
                
                # Crear template normalizado
                bpm = analysis.get('bpm') or 95.0
                
                # Detectar estilo del nombre del archivo
                style = self._detect_style_from_filename(wav_file.name)
                
                template = GrooveTemplate(
                    source_file=str(wav_file),
                    bpm=float(bpm),
                    kick_positions=groove_data.get('kick_positions', []),
                    snare_positions=groove_data.get('snare_positions', []),
                    hat_positions=groove_data.get('hat_positions', []),
                    kick_velocities=self._extract_velocities_for_positions(
                        groove_data.get('positions', []),
                        groove_data.get('velocities', []),
                        groove_data.get('kick_positions', [])
                    ),
                    snare_velocities=self._extract_velocities_for_positions(
                        groove_data.get('positions', []),
                        groove_data.get('velocities', []),
                        groove_data.get('snare_positions', [])
                    ),
                    hat_velocities=self._extract_velocities_for_positions(
                        groove_data.get('positions', []),
                        groove_data.get('velocities', []),
                        groove_data.get('hat_positions', [])
                    ),
                    timing_variance_ms=groove_data.get('timing_variance_ms', 0.0),
                    density=groove_data.get('density', 1.0),
                    style=style
                )
                
                self.templates[file_key] = template
                self._processed_hashes.add(file_hash)
                extracted_count += 1
                
                logger.info(f"  ✓ Extraído: {len(template.kick_positions)} kicks, "
                          f"{len(template.snare_positions)} snares, "
                          f"{len(template.hat_positions)} hats "
                          f"[{style} @ {bpm:.1f} BPM]")
                
            except Exception as e:
                logger.warning(f"  ⚠ Error analizando {wav_file.name}: {e}")
                error_count += 1
        
        if extracted_count > 0:
            self._save_cache()
            logger.info(f"✓ Extracción completa: {extracted_count} templates nuevos, "
                       f"{skipped_count} existentes, {error_count} errores")
        else:
            logger.info(f"No se encontraron templates nuevos. "
                       f"{skipped_count} ya existían, {error_count} errores")
        
        return extracted_count
    
    def _extract_velocities_for_positions(self, all_positions: List[float],
                                           all_velocities: List[float],
                                           target_positions: List[float]) -> List[float]:
        """Extrae velocidades correspondientes a posiciones específicas."""
        if not all_positions or not all_velocities:
            return [0.8] * len(target_positions)  # Default velocity
        
        result = []
        for target in target_positions:
            # Find closest position
            closest_idx = None
            min_dist = float('inf')
            for i, pos in enumerate(all_positions):
                dist = abs(pos - target)
                if dist < min_dist:
                    min_dist = dist
                    closest_idx = i
            
            if closest_idx is not None and closest_idx < len(all_velocities):
                result.append(all_velocities[closest_idx])
            else:
                result.append(0.8)
        
        return result
    
    def _detect_style_from_filename(self, filename: str) -> str:
        """
        Detecta el estilo de groove basado en el nombre del archivo.
        
        Args:
            filename: Nombre del archivo de audio
            
        Returns:
            Estilo detectado (dembow, mambo, pop, reggaeton, etc.)
        """
        name_lower = filename.lower()
        
        # Mapeo de palabras clave a estilos
        style_keywords = {
            'dembow': ['dembow', 'dembo', 'dembw'],
            'mambo': ['mambo', 'mambo_loop', 'mambo drums'],
            'perreo': ['perreo', 'perreo_loop', 'perreo drums'],
            'pop': ['pop', 'pop_loop', 'commercial'],
            'reggaeton': ['reggaeton', 'regueton', 'old school', 'antiguo'],
            'corte': ['corte', 'corte nes', 'nes'],
            'intro': ['intro', 'intro_loop', 'start'],
            'build': ['build', 'buildup', 'rise', 'riser'],
        }
        
        for style, keywords in style_keywords.items():
            for keyword in keywords:
                if keyword in name_lower:
                    return style
        
        # Default
        return "dembow"
    
    def get_template(self, bpm: Optional[float] = None, 
                     style: str = "dembow") -> Optional[GrooveTemplate]:
        """
        Obtiene un template de groove, opcionalmente filtrado por BPM.
        
        Args:
            bpm: BPM objetivo (busca templates cercanos)
            style: Estilo de groove (dembow, reggaeton, etc.)
            
        Returns:
            Template de groove o None si no hay disponibles
        """
        if not self.templates:
            # Intentar escanear si no hay templates
            self.scan_and_extract()
        
        if not self.templates:
            return None
        
        # Filtrar por estilo
        candidates = [t for t in self.templates.values() if t.style == style]
        if not candidates:
            candidates = list(self.templates.values())
        
        # Si hay BPM objetivo, buscar el más cercano
        if bpm:
            candidates.sort(key=lambda t: abs(t.bpm - bpm))
            return candidates[0] if candidates else None
        
        # Retornar uno aleatorio
        return random.choice(candidates) if candidates else None
    
    def get_template_for_section(self, section_kind: str, bpm: Optional[float] = None) -> Optional[GrooveTemplate]:
        """
        Obtiene un template apropiado para una sección específica.
        
        Las secciones intro/break usan templates más sparse,
        drop usa templates densos.
        """
        templates = list(self.templates.values())
        if not templates:
            return None
        
        # Filtrar por densidad según sección
        if section_kind in ['intro', 'break', 'outro']:
            # Buscar templates menos densos
            sparse = [t for t in templates if t.density < 4.0]
            candidates = sparse if sparse else templates
        elif section_kind == 'build':
            # Media densidad
            med = [t for t in templates if 4.0 <= t.density <= 6.0]
            candidates = med if med else templates
        else:  # drop
            # Alta densidad
            dense = [t for t in templates if t.density > 5.0]
            candidates = dense if dense else templates
        
        # Ordenar por cercanía de BPM si se especificó
        if bpm:
            candidates.sort(key=lambda t: abs(t.bpm - bpm))
        
        return candidates[0] if candidates else None
    
    def apply_to_drum_pattern(self, pattern: Dict[str, Any], 
                             template: GrooveTemplate,
                             intensity: float = 1.0) -> Dict[str, Any]:
        """
        Aplica un template de groove a un patrón de batería existente.
        
        Modifica las posiciones y velocidades según el groove extraído.
        
        Args:
            pattern: Patrón de batería original con 'kick', 'clap', 'hat_closed', etc.
            template: Template de groove a aplicar
            intensity: Intensidad de la aplicación (0.0-1.0)
            
        Returns:
            Patrón modificado con groove aplicado
        """
        result = dict(pattern)
        
        if intensity <= 0 or not template:
            return result
        
        # Aplicar posiciones del template con interpolación
        if 'kick' in result and template.kick_positions:
            result['kick'] = self._merge_positions(
                result['kick'], template.kick_positions, intensity
            )
        
        if 'clap' in result and template.snare_positions:
            result['clap'] = self._merge_positions(
                result['clap'], template.snare_positions, intensity
            )
        
        if 'hat_closed' in result and template.hat_positions:
            result['hat_closed'] = self._merge_positions(
                result['hat_closed'], template.hat_positions, intensity
            )
        
        return result
    
    def _merge_positions(self, original: List[float], template_pos: List[float],
                        intensity: float) -> List[float]:
        """Mezcla posiciones originales con las del template."""
        if intensity >= 0.9:
            # Usar casi completamente el template
            return sorted(template_pos)
        
        if intensity <= 0.1:
            # Usar casi completamente el original
            return sorted(original)
        
        # Interpolación: mantener hits fuertes del original, agregar variación del template
        # Encontrar hits que coincidan temporalmente
        merged = []
        
        for orig_hit in original:
            # Buscar hit cercano en el template
            closest_template = min(template_pos, key=lambda x: abs(x - orig_hit))
            distance = abs(closest_template - orig_hit)
            
            if distance < 0.25:  # Si están cerca, interpolar
                new_pos = orig_hit + (closest_template - orig_hit) * intensity
                merged.append(round(new_pos, 3))
            else:
                # Mantener hit original
                merged.append(orig_hit)
        
        # Agregar hits únicos del template si hay espacio
        for template_hit in template_pos:
            if not any(abs(template_hit - m) < 0.15 for m in merged):
                if random.random() < intensity * 0.5:  # Probabilidad de agregar
                    merged.append(template_hit)
        
        return sorted(merged)
    
    def list_available_templates(self) -> List[Dict[str, Any]]:
        """Lista templates disponibles con metadata incluyendo estilo."""
        return [
            {
                'source': Path(t.source_file).name,
                'bpm': t.bpm,
                'style': t.style,
                'kicks': len(t.kick_positions),
                'snares': len(t.snare_positions),
                'hats': len(t.hat_positions),
                'density': t.density,
                'timing_variance_ms': t.timing_variance_ms,
            }
            for t in self.templates.values()
        ]
    
    def clear_cache(self) -> None:
        """Limpia el cache de templates."""
        self.templates = {}
        if GROOVE_CACHE_PATH.exists():
            GROOVE_CACHE_PATH.unlink()
        logger.info("✓ Cache de groove limpiado")


# Instancia global
_groove_extractor: Optional[DembowGrooveExtractor] = None


def get_groove_extractor() -> DembowGrooveExtractor:
    """Obtiene la instancia global del extractor."""
    global _groove_extractor
    if _groove_extractor is None:
        _groove_extractor = DembowGrooveExtractor()
    return _groove_extractor


def extract_dembow_groove(force: bool = False) -> int:
    """
    Extrae groove de todos los loops dembow disponibles.
    
    Args:
        force: Si True, fuerza re-extracción
        
    Returns:
        Número de templates extraídos
    """
    extractor = get_groove_extractor()
    return extractor.scan_and_extract(force_reextract=force)


def get_dembow_groove(bpm: Optional[float] = None, 
                      section: Optional[str] = None) -> Optional[Dict[str, Any]]:
    """
    Obtiene un template de groove dembow.
    
    Args:
        bpm: BPM objetivo
        section: Sección ('intro', 'build', 'drop', 'break', 'outro')
        
    Returns:
        Template como dict o None
    """
    extractor = get_groove_extractor()
    
    if section:
        template = extractor.get_template_for_section(section, bpm)
    else:
        template = extractor.get_template(bpm)
    
    return template.to_dict() if template else None


def apply_groove_to_pattern(pattern: Dict[str, Any], 
                           groove_template: Dict[str, Any],
                           intensity: float = 0.7) -> Dict[str, Any]:
    """
    Aplica un groove template a un patrón de batería.
    
    Args:
        pattern: Patrón con 'kick', 'clap', 'hat_closed'
        groove_template: Template de groove como dict
        intensity: Intensidad de aplicación (0.0-1.0)
        
    Returns:
        Patrón modificado
    """
    extractor = get_groove_extractor()
    template = GrooveTemplate.from_dict(groove_template)
    return extractor.apply_to_drum_pattern(pattern, template, intensity)


def list_groove_templates() -> List[Dict[str, Any]]:
    """Lista todos los templates de groove disponibles."""
    extractor = get_groove_extractor()
    return extractor.list_available_templates()


# Testing
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    print("\n" + "="*60)
    print("EXTRACTOR DE GROOVE DEMBOW")
    print("="*60)
    
    # Extraer templates
    count = extract_dembow_groove()
    print(f"\nTemplates extraídos: {count}")
    
    # Listar disponibles
    templates = list_groove_templates()
    print(f"\nTemplates disponibles: {len(templates)}")
    for t in templates[:5]:  # Mostrar primeros 5
        print(f"  - {t['source']} ({t['bpm']} BPM)")
        print(f"    {t['kicks']} kicks, {t['snares']} snares, {t['hats']} hats")
        print(f"    densidad: {t['density']:.2f}, variance: {t['timing_variance_ms']:.1f}ms")
    
    # Obtener un template de ejemplo
    template = get_dembow_groove(bpm=95.0, section='drop')
    if template:
        print(f"\nTemplate de ejemplo (95 BPM drop):")
        print(f"  Kicks: {template['kick_positions']}")
        print(f"  Snares: {template['snare_positions']}")
        print(f"  Hats: {template['hat_positions']}")
