"""
Sample Selector - Intelligent sample selection with metadata store integration.

Indexes libreria/reggaeton and returns sample packs by genre with support for:
- Database-first queries with SQLite caching
- Graceful degradation when numpy is unavailable
- Hybrid analysis with automatic caching

Usage:
    from engines.sample_selector import SampleSelector, get_selector
    
    # With metadata store
    selector = SampleSelector(metadata_store=store)
    samples = selector.select_for_genre("reggaeton")
    
    # Without numpy (database-only mode)
    samples = selector.get_samples_without_numpy("kick", count=10)
"""

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

logger = logging.getLogger("SampleSelector")

# Senior Architecture: Check numpy availability
NUMPY_AVAILABLE = False
try:
    import numpy as np
    NUMPY_AVAILABLE = True
except ImportError:
    pass

LIBROSA_AVAILABLE = False
try:
    import librosa
    LIBROSA_AVAILABLE = True
except ImportError:
    pass

# Import new metadata store and abstract analyzer
from .metadata_store import SampleMetadataStore, SampleFeatures, create_metadata_store
from .abstract_analyzer import (
    HybridExtractor, 
    DatabaseExtractor, 
    create_extractor
)

REGGAETON_DIR = Path(
    r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\libreria\reggaeton"
)

_ROLE_MAP = {
    "kick": ["kick"],
    "snare": ["snare"],
    "clap": ["snare", "clap"],
    "hat_closed": ["hi-hat"],
    "hat_open": ["hi-hat"],
    "bass": ["bass"],
    "synth": ["oneshots", "reggaeton 3"],
    "fx": ["fx"],
    "perc": ["perc loop", "hi-hat"],
}


@dataclass
class SampleInfo:
    name: str
    path: str
    role: str
    pack: str = ""
    key: str = ""
    bpm: float = 0.0
    
    @classmethod
    def from_sample_features(cls, features: SampleFeatures, role: str = "") -> "SampleInfo":
        """Create SampleInfo from SampleFeatures."""
        return cls(
            name=Path(features.path).name,
            path=features.path,
            role=role or (features.categories[0] if features.categories else "unknown"),
            pack=Path(features.path).parent.name,
            key=features.key or "",
            bpm=features.bpm or 0.0
        )


@dataclass
class DrumKit:
    name: str
    kick: Optional[SampleInfo] = None
    snare: Optional[SampleInfo] = None
    clap: Optional[SampleInfo] = None
    hat_closed: Optional[SampleInfo] = None
    hat_open: Optional[SampleInfo] = None


@dataclass
class InstrumentGroup:
    genre: str
    key: str
    bpm: float
    drums: Optional[DrumKit] = None
    bass: List[SampleInfo] = field(default_factory=list)
    synths: List[SampleInfo] = field(default_factory=list)
    fx: List[SampleInfo] = field(default_factory=list)

    def __post_init__(self):
        if self.drums is None:
            self.drums = DrumKit(name="%s Kit" % self.genre.title())


class SampleSelector:
    """
    Intelligent sample selector with metadata store integration.
    
    Supports two modes:
    - Full mode (numpy available): Database + audio analysis with caching
    - Database-only mode: SQLite queries without audio analysis
    """

    def __init__(
        self, 
        library_path: Optional[str] = None,
        metadata_store: Optional[SampleMetadataStore] = None,
        embedding_engine=None,
        reference_matcher=None,
        verbose: bool = False
    ):
        """
        Initialize sample selector.
        
        Args:
            library_path: Path to sample library (default: libreria/reggaeton)
            metadata_store: Optional metadata store instance
            embedding_engine: Optional embedding engine for similarity search
            reference_matcher: Optional reference matcher for style matching
            verbose: Enable verbose logging
        """
        self._library = Path(library_path) if library_path else REGGAETON_DIR
        self._index: List[SampleInfo] = []
        self._indexed = False
        self.verbose = verbose
        self.embedding_engine = embedding_engine
        self.reference_matcher = reference_matcher
        
        # Senior Architecture: Metadata store integration
        if metadata_store is None and NUMPY_AVAILABLE:
            # Only create metadata store if we can populate it
            db_path = str(self._library.parent / "sample_metadata.db")
            self.metadata_store = create_metadata_store(db_path)
            if self.verbose:
                logger.info(f"[SampleSelector] Created metadata store at {db_path}")
        elif metadata_store is not None:
            self.metadata_store = metadata_store
            if self.verbose:
                logger.info("[SampleSelector] Using provided metadata store")
        else:
            self.metadata_store = None
            logger.warning("[SampleSelector] No metadata store available")
        
        # Initialize extractor (Hybrid or Database-only based on numpy availability)
        self.extractor = create_extractor(self.metadata_store, verbose=verbose)
        
        # Track extraction mode
        if metadata_store:
            self._extraction_mode = "database_first"
            self.extraction_mode = "database_first"
        elif NUMPY_AVAILABLE and LIBROSA_AVAILABLE:
            self._extraction_mode = "full_analysis"
            self.extraction_mode = "full_analysis"
        else:
            self._extraction_mode = "limited"
            self.extraction_mode = "limited"
        
        if verbose:
            logger.info(f"[SampleSelector] Mode: {self.extraction_mode}")
        
        if not NUMPY_AVAILABLE:
            logger.warning("[SampleSelector] Running in DATABASE-ONLY mode (numpy unavailable)")
        elif not LIBROSA_AVAILABLE:
            logger.warning("[SampleSelector] Running in LIMITED mode (librosa unavailable)")
        else:
            logger.info("[SampleSelector] Running in FULL mode (numpy + librosa available)")

    def _build_index(self):
        """Build index from filesystem."""
        if self._indexed:
            return
        self._index = []
        if not self._library.is_dir():
            logger.warning("Library not found: %s", self._library)
            return

        for root, _dirs, files in os.walk(self._library):
            for f in files:
                if f.lower().endswith((".wav", ".aif", ".aiff", ".mp3", ".flac")):
                    fpath = os.path.join(root, f)
                    rel = os.path.relpath(root, str(self._library))
                    pack = rel.split(os.sep)[0] if rel else "unknown"
                    role = self._guess_role(f, rel)
                    self._index.append(SampleInfo(
                        name=f, path=fpath, role=role, pack=pack
                    ))
        self._indexed = True
        logger.info("Indexed %d samples from %s", len(self._index), self._library)

    def _guess_role(self, filename: str, relpath: str) -> str:
        """Guess sample role from filename and path."""
        lower = filename.lower()
        rel = relpath.lower()
        if "kick" in lower or "kick" in rel:
            return "kick"
        if "snare" in lower or "snare" in rel:
            return "snare"
        if "clap" in lower:
            return "clap"
        if "hi-hat" in rel or "hihat" in lower:
            return "hat_closed"
        if "bass" in lower or "bass" in rel:
            return "bass"
        if "fx" in lower or "fx" in rel:
            return "fx"
        if "perc" in lower or "perc" in rel:
            return "perc"
        if "drumloop" in rel:
            return "drum_loop"
        return "synth"

    def _get_samples(self, role: str, limit: int = 10) -> List[SampleInfo]:
        """Get samples by role from filesystem index."""
        self._build_index()
        dirs = _ROLE_MAP.get(role, [])
        results = [s for s in self._index if s.role == role or s.pack in dirs]
        return results[:limit]

    def select_samples_db_only(self, role, count=10, bpm_range=None, key=None):
        """Select samples using only database (no numpy/librosa).
        
        Args:
            role: Sample role (kick, snare, bass, etc.)
            count: Number of samples to return
            bpm_range: Optional (min, max) BPM range
            key: Optional musical key
        
        Returns:
            List of SampleInfo objects from database
        """
        if not self.metadata_store:
            logger.error("Metadata store not available")
            return []
        
        # Query database for samples matching criteria
        features_list = self.metadata_store.get_samples_by_category(role)
        
        # Filter by BPM range if specified
        if bpm_range and len(bpm_range) == 2:
            min_bpm, max_bpm = bpm_range
            features_list = [
                f for f in features_list 
                if min_bpm <= f.bpm <= max_bpm
            ]
        
        # Filter by key if specified
        if key:
            features_list = [
                f for f in features_list 
                if f.key == key
            ]
        
        # Convert to SampleInfo
        results = []
        for features in features_list[:count]:
            info = SampleInfo(
                path=features.path,
                name=os.path.basename(features.path),
                role=role,
                pack=os.path.basename(os.path.dirname(features.path)),
                key=features.key or "",
                bpm=features.bpm or 0.0
            )
            results.append(info)
        
        return results

    def _get_samples_librosa(self, role: str, count: int = 10, **kwargs) -> List[SampleInfo]:
        """Get samples using librosa audio analysis.
        
        This method requires numpy and librosa for audio feature extraction.
        Used as fallback when database has no cached samples.
        
        Args:
            role: Sample role (kick, snare, bass, etc.)
            count: Number of samples to return
            **kwargs: Additional filter parameters (target_bpm, target_key, etc.)
            
        Returns:
            List of SampleInfo objects from audio analysis
        """
        if not NUMPY_AVAILABLE or not LIBROSA_AVAILABLE:
            logger.error("Librosa analysis requested but numpy/librosa not available")
            return []
        
        # Get filesystem samples for this role
        fs_samples = self._get_samples(role, count * 2)
        results = []
        
        target_bpm = kwargs.get('target_bpm')
        target_key = kwargs.get('target_key')
        
        for sample in fs_samples:
            try:
                # Analyze audio with librosa
                features = self.extractor.extract(sample.path)
                if features:
                    # Filter by BPM if specified
                    if target_bpm and features.bpm:
                        if abs(features.bpm - target_bpm) > 10:
                            continue
                    # Filter by key if specified
                    if target_key and features.key:
                        if features.key != target_key:
                            continue
                    
                    sample_info = SampleInfo.from_sample_features(features, role=role)
                    results.append(sample_info)
                else:
                    # Analysis failed, use filesystem sample with basic info
                    results.append(sample)
            except Exception as e:
                logger.warning(f"[SampleSelector] Librosa analysis failed for {sample.path}: {e}")
                results.append(sample)
            
            if len(results) >= count:
                break
        
        return results[:count]

    def get_samples_without_numpy(self, role: str, count: int = 10) -> List[SampleInfo]:
        """
        Get samples using only SQLite database, no audio analysis.
        
        This method works entirely without numpy/librosa by querying
        the pre-populated metadata database.
        
        Args:
            role: Sample role (kick, snare, bass, etc.)
            count: Number of samples to return
            
        Returns:
            List of SampleInfo objects from database
        """
        logger.info(f"[SampleSelector] Database-only query for role: {role}")
        
        # Check if metadata_store is available
        if self.metadata_store is None:
            logger.warning(f"[SampleSelector] No metadata store available, using filesystem fallback for {role}")
            return self._get_samples(role, count)
        
        # Map role to database category
        categories = _ROLE_MAP.get(role, [role])
        results = []
        
        # Search database for each category
        for category in categories:
            db_results = self.metadata_store.search_samples(
                category=category,
                limit=count
            )
            
            for features in db_results:
                sample_info = SampleInfo.from_sample_features(features, role=role)
                results.append(sample_info)
            
            if len(results) >= count:
                break
        
        # If no database results, fall back to filesystem
        if not results:
            logger.warning(f"[SampleSelector] No database results for {role}, using filesystem fallback")
            return self._get_samples(role, count)
        
        logger.info(f"[SampleSelector] Found {len(results[:count])} samples for {role} (database-only)")
        return results[:count]

    def select_by_similarity(self, reference_path: str, top_n: int = 10) -> InstrumentGroup:
        """Select samples similar to a reference audio file."""
        try:
            # Import here to avoid circular dependencies
            from . import embedding_engine as ee
            
            # Find similar samples using embeddings
            similar = ee.find_similar(reference_path, top_n=top_n * 3)
            
            if not similar:
                logger.warning("No similar samples found for %s, falling back to random", reference_path)
                return self.select_for_genre("reggaeton")
            
            # Build index if not already done
            self._build_index()
            
            # Get reference features using extractor (database-first, then analysis)
            ref_features = self.extractor.get_features(reference_path)
            ref_bpm = ref_features.get("bpm", 95.0) if ref_features else 95.0
            ref_key = ref_features.get("key", "Am") if ref_features else "Am"
            
            group = InstrumentGroup(genre="similar_to_reference", key=ref_key, bpm=ref_bpm)
            
            # Filter similar samples by role
            kick_samples = [s for s in similar if s.role == "kick"][:3]
            snare_samples = [s for s in similar if s.role in ("snare", "clap")][:3]
            hat_samples = [s for s in similar if s.role in ("hat_closed", "hat_open")][:3]
            bass_samples = [s for s in similar if s.role == "bass"][:5]
            synth_samples = [s for s in similar if s.role in ("synth", "oneshot")][:5]
            fx_samples = [s for s in similar if s.role == "fx"][:3]
            
            # Build drum kit
            group.drums = DrumKit(
                name="Similar Kit",
                kick=kick_samples[0] if kick_samples else None,
                snare=snare_samples[0] if snare_samples else None,
                clap=snare_samples[1] if len(snare_samples) > 1 else None,
                hat_closed=hat_samples[0] if hat_samples else None,
                hat_open=hat_samples[1] if len(hat_samples) > 1 else None,
            )
            
            # Fill other instruments
            group.bass = bass_samples
            group.synths = synth_samples
            group.fx = fx_samples
            
            logger.info("Selected %d similar samples for reference: %s", 
                       len([x for x in [group.drums.kick, group.drums.snare] + group.bass + group.synths + group.fx if x]),
                       reference_path)
            
            return group
            
        except Exception as e:
            logger.error("Error in select_by_similarity: %s", str(e))
            return self.select_for_genre("reggaeton")

    def select_for_genre(
        self, 
        genre: str, 
        key: Optional[str] = None,
        bpm: Optional[float] = None
    ) -> InstrumentGroup:
        """
        Select a complete sample pack for the given genre.
        
        Uses database-first approach: queries SQLite for cached samples,
        only analyzing new samples if numpy is available.
        
        Args:
            genre: Genre to select samples for
            key: Musical key (default: Am)
            bpm: Tempo in BPM (default: 95.0)
            
        Returns:
            InstrumentGroup with selected samples
        """
        self._build_index()
        if not self._index:
            raise ValueError("No samples found in %s" % self._library)

        group = InstrumentGroup(genre=genre, key=key or "Am", bpm=bpm or 95.0)

        # Try database-first for each role, fallback to filesystem
        if isinstance(self.extractor, DatabaseExtractor) or not NUMPY_AVAILABLE:
            # Database-only mode
            logger.info("[SampleSelector] Using database-only selection")
            kick = self.get_samples_without_numpy("kick", 3)
            snare = self.get_samples_without_numpy("snare", 3)
            clap = self.get_samples_without_numpy("clap", 2)
            hats = self.get_samples_without_numpy("hat_closed", 4)
            bass = self.get_samples_without_numpy("bass", 5)
            synths = self.get_samples_without_numpy("synth", 5)
            fx = self.get_samples_without_numpy("fx", 3)
        else:
            # Hybrid mode: database first, then analyze uncached samples
            logger.info("[SampleSelector] Using hybrid selection (database + analysis)")
            
            kick = self._get_samples_hybrid("kick", 3)
            snare = self._get_samples_hybrid("snare", 3)
            clap = self._get_samples_hybrid("clap", 2)
            hats = self._get_samples_hybrid("hat_closed", 4)
            bass = self._get_samples_hybrid("bass", 5)
            synths = self._get_samples_hybrid("synth", 5)
            fx = self._get_samples_hybrid("fx", 3)

        # Build drum kit
        group.drums = DrumKit(
            name="%s Kit" % genre.title(),
            kick=kick[0] if kick else None,
            snare=snare[0] if snare else None,
            clap=clap[0] if clap else (snare[1] if len(snare) > 1 else None),
            hat_closed=hats[0] if hats else None,
            hat_open=hats[1] if len(hats) > 1 else None,
        )

        # Fill other instruments
        group.bass = bass
        group.synths = synths
        group.fx = fx

        return group

    def _get_samples_hybrid(self, role: str, count: int) -> List[SampleInfo]:
        """
        Get samples using hybrid approach: database first, analyze if needed.
        
        Args:
            role: Sample role
            count: Number of samples needed
            
        Returns:
            List of SampleInfo objects
        """
        results = []
        
        # Get filesystem samples for this role
        fs_samples = self._get_samples(role, count * 2)
        
        for sample in fs_samples:
            # Try database first
            db_features = self.metadata_store.get_sample_features(sample.path)
            
            if db_features:
                # Cache hit - use database result
                sample_info = SampleInfo.from_sample_features(db_features, role=role)
                results.append(sample_info)
            elif NUMPY_AVAILABLE and LIBROSA_AVAILABLE:
                # Cache miss - analyze and cache
                try:
                    features = self.extractor.extract(sample.path)
                    if features:
                        sample_info = SampleInfo.from_sample_features(features, role=role)
                        results.append(sample_info)
                    else:
                        # Analysis failed, use filesystem sample
                        results.append(sample)
                except Exception as e:
                    logger.warning(f"[SampleSelector] Analysis failed for {sample.path}: {e}")
                    results.append(sample)
            else:
                # No numpy available, use filesystem sample
                results.append(sample)
            
            if len(results) >= count:
                break
        
        return results[:count]

    def get_recommended_samples(self, role, count=10, **kwargs):
        """Get recommended samples with database-first approach."""
        # Try database first
        if self.metadata_store:
            target_bpm = kwargs.get('target_bpm')
            target_key = kwargs.get('target_key')
            
            bpm_range = None
            if target_bpm:
                bpm_range = (target_bpm - 5, target_bpm + 5)
            
            db_results = self.select_samples_db_only(role, count, bpm_range=bpm_range, key=target_key)
            if db_results:
                logger.info(f"Retrieved {len(db_results)} samples from database")
                return db_results
        
        # Fall back to legacy analysis if numpy available
        if NUMPY_AVAILABLE and LIBROSA_AVAILABLE:
            logger.info("Using librosa analysis for samples")
            return self._get_samples_librosa(role, count, **kwargs)
        
        # Limited mode: return empty with warning
        logger.warning("No metadata store and no numpy - cannot select samples")
        return []


# Global instance
_selector: Optional[SampleSelector] = None


def get_selector(
    library_path: Optional[str] = None,
    metadata_store: Optional[SampleMetadataStore] = None
) -> SampleSelector:
    """
    Get global SampleSelector instance.
    
    Args:
        library_path: Optional library path
        metadata_store: Optional metadata store
        
    Returns:
        SampleSelector singleton
    """
    global _selector
    if _selector is None:
        _selector = SampleSelector(library_path, metadata_store)
    return _selector


def select_samples_for_track(
    genre: str, 
    key: str = "", 
    bpm: float = 0,
    metadata_store: Optional[SampleMetadataStore] = None
) -> InstrumentGroup:
    """
    Convenience function: select samples for a genre.
    
    Args:
        genre: Genre to select
        key: Musical key
        bpm: Tempo in BPM
        metadata_store: Optional metadata store
        
    Returns:
        InstrumentGroup with selected samples
    """
    return get_selector(metadata_store=metadata_store).select_for_genre(
        genre, 
        key if key else None, 
        bpm if bpm > 0 else None
    )


def get_drum_kit(
    genre: str = "reggaeton", 
    variation: str = "standard",
    metadata_store: Optional[SampleMetadataStore] = None
) -> DrumKit:
    """
    Get a drum kit for the genre.
    
    Args:
        genre: Genre for drum kit
        variation: Kit variation style
        metadata_store: Optional metadata store
        
    Returns:
        DrumKit with selected samples
    """
    group = get_selector(metadata_store=metadata_store).select_for_genre(genre)
    return group.drums


def get_recommended_samples(
    role: str,
    count: int = 5,
    target_bpm: Optional[float] = None,
    target_key: Optional[str] = None,
    metadata_store: Optional[SampleMetadataStore] = None
) -> List[SampleInfo]:
    """
    Get recommended samples for a role from metadata store.
    
    Args:
        role: Sample role/category
        count: Number of samples
        target_bpm: Optional BPM target
        target_key: Optional key target
        metadata_store: Optional metadata store
        
    Returns:
        List of recommended SampleInfo objects
    """
    return get_selector(metadata_store=metadata_store).get_recommended_samples(
        role=role,
        count=count,
        target_bpm=target_bpm,
        target_key=target_key
    )


def reset_cross_generation_memory():
    """Reset selection memory (compatibility stub)."""
    pass


def get_extraction_mode() -> str:
    """
    Get current extraction mode for debugging.
    
    Returns:
        Mode string: "full_analysis", "limited_analysis", "database_only", etc.
    """
    selector = get_selector()
    return selector.extraction_mode


def is_numpy_available() -> bool:
    """Check if numpy is available for analysis."""
    return NUMPY_AVAILABLE


def is_librosa_available() -> bool:
    """Check if librosa is available for analysis."""
    return LIBROSA_AVAILABLE


# ==================== BPM-Aware Selector (Phase 4-5) ====================

class BPMAwareSelector:
    """Selects samples based on BPM coherence and spectral similarity."""
    
    def __init__(self, metadata_store, bpm_analyzer=None, spectral_coherence=None):
        self.store = metadata_store
        self.bpm_analyzer = bpm_analyzer
        self.spectral = spectral_coherence
    
    def select_for_bpm(
        self, 
        target_bpm: float, 
        category: str = None,
        pool_size: int = 20,
        tolerance: float = 5.0
    ) -> List[str]:
        """
        Select samples within BPM tolerance.
        
        Priority:
        1. Samples with BPM within tolerance (±5 BPM default)
        2. Sort by confidence score
        3. Return top pool_size samples
        """
        if not self.store:
            logger.error("Metadata store not available for BPM selection")
            return []
        
        try:
            conn = self.store._get_connection()
            cursor = conn.cursor()
            
            min_bpm = target_bpm - tolerance
            max_bpm = target_bpm + tolerance
            
            if category:
                # Filter by category and BPM range
                cursor.execute("""
                    SELECT path FROM samples_bpm 
                    WHERE category = ? AND bpm >= ? AND bpm <= ?
                    ORDER BY confidence DESC, ABS(bpm - ?) ASC
                    LIMIT ?
                """, (category, min_bpm, max_bpm, target_bpm, pool_size))
            else:
                # Filter by BPM range only
                cursor.execute("""
                    SELECT path FROM samples_bpm 
                    WHERE bpm >= ? AND bpm <= ?
                    ORDER BY confidence DESC, ABS(bpm - ?) ASC
                    LIMIT ?
                """, (min_bpm, max_bpm, target_bpm, pool_size))
            
            results = [row['path'] for row in cursor.fetchall()]
            
            logger.info(f"Selected {len(results)} samples for {target_bpm} BPM "
                       f"(tolerance: ±{tolerance}, category: {category or 'any'})")
            
            return results
            
        except Exception as e:
            logger.error(f"Error in BPM selection: {e}")
            return []
    
    def select_with_spectral_coherence(
        self,
        target_bpm: float,
        reference_sample: str,
        category: str = None,
        top_k: int = 10
    ) -> List[Tuple[str, float]]:
        """
        Select samples that match both BPM and spectral profile.
        
        Returns: List of (path, coherence_score)
        """
        if not self.store:
            logger.error("Metadata store not available for spectral selection")
            return []
        
        try:
            # First, get samples in BPM range
            bpm_pool = self.select_for_bpm(target_bpm, category, pool_size=50, tolerance=5.0)
            
            if not bpm_pool:
                logger.warning(f"No samples found in BPM range for {target_bpm}")
                return []
            
            # Get spectral similarities from reference
            similar_samples = self.store.get_similar_by_spectral(reference_sample, top_k=50)
            
            # Create a set of BPM-matching paths for fast lookup
            bpm_pool_set = set(bpm_pool)
            
            # Filter similarities to only include BPM-matching samples
            coherent_samples = [
                (path, score) for path, score in similar_samples 
                if path in bpm_pool_set
            ]
            
            # Sort by coherence score and return top_k
            coherent_samples.sort(key=lambda x: x[1], reverse=True)
            
            logger.info(f"Found {len(coherent_samples)} samples matching both BPM and spectral profile")
            
            return coherent_samples[:top_k]
            
        except Exception as e:
            logger.error(f"Error in spectral coherence selection: {e}")
            return []
    
    def recommend_warp_mode(
        self, 
        sample_bpm: float, 
        target_bpm: float
    ) -> str:
        """
        Recommend warp mode based on BPM difference.
        
        Returns: 'complex_pro', 'complex', or 'beats'
        """
        delta = abs(sample_bpm - target_bpm)
        delta_pct = delta / target_bpm * 100 if target_bpm > 0 else 0
        
        if delta_pct <= 5:
            return 'complex_pro'  # High quality for small changes
        elif delta_pct <= 10:
            return 'complex'  # Good quality for moderate changes
        else:
            return 'beats'  # Best for percussive with large changes
    
    def get_warp_recommendations(
        self,
        sample_paths: List[str],
        target_bpm: float
    ) -> Dict[str, str]:
        """
        Get warp mode recommendations for multiple samples.
        
        Args:
            sample_paths: List of sample paths
            target_bpm: Target BPM
            
        Returns:
            Dictionary mapping sample paths to recommended warp modes
        """
        recommendations = {}
        
        for path in sample_paths:
            # Get sample BPM from store
            try:
                conn = self.store._get_connection()
                cursor = conn.cursor()
                cursor.execute(
                    "SELECT bpm FROM samples_bpm WHERE path = ?", 
                    (path,)
                )
                row = cursor.fetchone()
                
                if row and row['bpm']:
                    sample_bpm = row['bpm']
                else:
                    sample_bpm = target_bpm  # Default to no warp needed
                
                recommendations[path] = self.recommend_warp_mode(sample_bpm, target_bpm)
                
            except Exception as e:
                logger.warning(f"Could not get warp recommendation for {path}: {e}")
                recommendations[path] = 'complex'  # Safe default
        
        return recommendations
