"""
PresetManager - Save/Load Coherent Sample Kits

Manages coherent sample kit presets with CRUD operations,
similarity matching, and usage tracking.
"""

import os
import json
import time
import hashlib
import shutil
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, asdict


@dataclass
class SampleEntry:
    """Represents a sample in a kit with variations."""
    base: str
    variations: Dict[str, str] = None
    
    def __post_init__(self):
        if self.variations is None:
            self.variations = {}
    
    def to_dict(self) -> Dict:
        return {
            "base": self.base,
            "variations": self.variations
        }
    
    @classmethod
    def from_dict(cls, data: Dict) -> 'SampleEntry':
        return cls(
            base=data.get("base", ""),
            variations=data.get("variations", {})
        )


@dataclass
class CoherenceProof:
    """Coherence verification data for a kit."""
    overall_score: float
    pair_scores: List[Dict[str, Any]]
    
    def to_dict(self) -> Dict:
        return {
            "overall_score": self.overall_score,
            "pair_scores": self.pair_scores
        }
    
    @classmethod
    def from_dict(cls, data: Dict) -> 'CoherenceProof':
        return cls(
            overall_score=data.get("overall_score", 0.0),
            pair_scores=data.get("pair_scores", [])
        )


@dataclass
class KitMetadata:
    """Metadata for a sample kit preset."""
    genre: str
    style: str
    tempo: int
    key: str
    coherence_score: float
    variation_level: str = "medium"
    tags: List[str] = None
    
    def __post_init__(self):
        if self.tags is None:
            self.tags = []
    
    def to_dict(self) -> Dict:
        return {
            "genre": self.genre,
            "style": self.style,
            "tempo": self.tempo,
            "key": self.key,
            "coherence_score": self.coherence_score,
            "variation_level": self.variation_level,
            "tags": self.tags
        }
    
    @classmethod
    def from_dict(cls, data: Dict) -> 'KitMetadata':
        return cls(
            genre=data.get("genre", "unknown"),
            style=data.get("style", "standard"),
            tempo=data.get("tempo", 95),
            key=data.get("key", "Am"),
            coherence_score=data.get("coherence_score", 0.0),
            variation_level=data.get("variation_level", "medium"),
            tags=data.get("tags", [])
        )


@dataclass
class Preset:
    """Complete preset structure for a coherent sample kit."""
    name: str
    description: str
    created_at: str
    metadata: KitMetadata
    kit: Dict[str, SampleEntry]
    coherence_proof: CoherenceProof
    usage_count: int = 0
    last_used: str = ""
    
    def to_dict(self) -> Dict:
        return {
            "name": self.name,
            "description": self.description,
            "created_at": self.created_at,
            "metadata": self.metadata.to_dict(),
            "kit": {k: v.to_dict() for k, v in self.kit.items()},
            "coherence_proof": self.coherence_proof.to_dict(),
            "usage_count": self.usage_count,
            "last_used": self.last_used
        }
    
    @classmethod
    def from_dict(cls, data: Dict) -> 'Preset':
        return cls(
            name=data.get("name", "Unnamed"),
            description=data.get("description", ""),
            created_at=data.get("created_at", ""),
            metadata=KitMetadata.from_dict(data.get("metadata", {})),
            kit={k: SampleEntry.from_dict(v) for k, v in data.get("kit", {}).items()},
            coherence_proof=CoherenceProof.from_dict(data.get("coherence_proof", {})),
            usage_count=data.get("usage_count", 0),
            last_used=data.get("last_used", "")
        )


class PresetManager:
    """
    Manages coherent sample kit presets with save/load/search capabilities.
    
    Features:
    - CRUD operations for presets
    - Search and filter by genre, style, coherence
    - Similarity matching between kits
    - Usage tracking
    - Duplicate detection
    - Import/export for sharing
    """
    
    def __init__(self, presets_dir: Optional[str] = None):
        """
        Initialize PresetManager.
        
        Args:
            presets_dir: Directory for preset storage. If None, uses default.
        """
        if presets_dir is None:
            # Default to AbletonMCP_AI/presets/
            base_dir = Path(__file__).parent.parent.parent
            self.presets_dir = base_dir / "presets"
        else:
            self.presets_dir = Path(presets_dir)
        
        # Ensure directory exists
        self.presets_dir.mkdir(parents=True, exist_ok=True)
        
        # Cache for loaded presets
        self._cache: Dict[str, Preset] = {}
        self._cache_timestamp: Optional[datetime] = None
    
    def _generate_filename(self, metadata: KitMetadata) -> str:
        """
        Generate filename from metadata.
        
        Format: {genre}_{style}_{coherence}_{timestamp}.json
        """
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        coherence_str = f"{metadata.coherence_score:.2f}"
        safe_genre = metadata.genre.replace(" ", "_").lower()
        safe_style = metadata.style.replace(" ", "_").lower()
        return f"{safe_genre}_{safe_style}_{coherence_str}_{timestamp}.json"
    
    def _generate_name(self, metadata: KitMetadata, kit: Dict[str, SampleEntry]) -> str:
        """
        Auto-generate meaningful preset name.
        
        Based on genre, style, key elements in kit.
        """
        # Base name from style
        base_name = metadata.style.replace("_", " ").title()
        
        # Add descriptors based on kit contents
        descriptors = []
        
        if "kick" in kit:
            kick_path = kit["kick"].base.lower()
            if "pesado" in kick_path or "heavy" in kick_path:
                descriptors.append("Pesado")
            elif "sutil" in kick_path or "soft" in kick_path:
                descriptors.append("Suave")
            elif "estampido" in kick_path:
                descriptors.append("Estampido")
        
        if "bass" in kit:
            descriptors.append("Con Bajo")
        
        # Add coherence quality
        if metadata.coherence_score >= 0.95:
            descriptors.append("Ultra")
        elif metadata.coherence_score >= 0.90:
            descriptors.append("Premium")
        
        # Combine
        if descriptors:
            descriptor_str = ", ".join(descriptors[:2])  # Max 2 descriptors
            name = f"{base_name} ({descriptor_str})"
        else:
            name = base_name
        
        # Add uniqueness number
        existing = self._get_existing_names()
        count = 1
        final_name = name
        while final_name in existing:
            count += 1
            final_name = f"{name} #{count}"
        
        return final_name
    
    def _generate_description(self, metadata: KitMetadata, kit: Dict[str, SampleEntry]) -> str:
        """Generate human-readable description."""
        parts = [
            f"{metadata.tempo}bpm {metadata.key}",
        ]
        
        # Describe key elements
        elements = []
        if "kick" in kit:
            kick_file = os.path.basename(kit["kick"].base)
            elements.append(f"kick: {kick_file.replace('.wav', '').replace('_', ' ')}")
        if "snare" in kit:
            elements.append("snare incluido")
        if "bass" in kit:
            elements.append("bass presente")
        
        if elements:
            parts.append(", ".join(elements))
        
        # Add energy description
        if metadata.coherence_score >= 0.95:
            parts.append("coherencia excepcional")
        elif metadata.coherence_score >= 0.90:
            parts.append("alta coherencia")
        
        return " | ".join(parts)
    
    def _get_existing_names(self) -> set:
        """Get set of existing preset names."""
        names = set()
        for filename in self.presets_dir.glob("*.json"):
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    names.add(data.get("name", ""))
            except:
                pass
        return names
    
    def _compute_kit_hash(self, kit: Dict[str, SampleEntry]) -> str:
        """
        Compute hash for kit to detect duplicates.
        
        Uses base sample paths only (not variations).
        """
        # Extract base paths and sort for consistency
        base_paths = []
        for role in sorted(kit.keys()):
            entry = kit[role]
            base_paths.append(f"{role}:{entry.base}")
        
        # Create hash
        content = "|".join(base_paths)
        return hashlib.md5(content.encode()).hexdigest()[:16]
    
    def _check_duplicate(self, kit: Dict[str, SampleEntry]) -> Optional[str]:
        """
        Check if kit already exists as a preset.
        
        Returns preset name if duplicate found, None otherwise.
        """
        kit_hash = self._compute_kit_hash(kit)
        
        for filename in self.presets_dir.glob("*.json"):
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    existing_kit = data.get("kit", {})
                    existing_hash = self._compute_kit_hash(
                        {k: SampleEntry.from_dict(v) for k, v in existing_kit.items()}
                    )
                    if existing_hash == kit_hash:
                        return data.get("name")
            except:
                pass
        
        return None
    
    def save_preset(
        self,
        name: Optional[str],
        kit: Dict[str, Any],
        coherence_score: float,
        metadata: Dict[str, Any],
        coherence_proof: Optional[Dict] = None,
        allow_duplicates: bool = False
    ) -> Tuple[bool, str, Preset]:
        """
        Save a new preset.
        
        Args:
            name: Preset name (auto-generated if None)
            kit: Dictionary of role -> {base: path, variations: {context: path}}
            coherence_score: Overall coherence score (0.0-1.0)
            metadata: Dict with genre, style, tempo, key, etc.
            coherence_proof: Optional detailed coherence data
            allow_duplicates: If False, checks for existing identical kits
        
        Returns:
            Tuple of (success: bool, message: str, preset: Preset)
        """
        # Convert kit to SampleEntry objects
        kit_entries = {}
        for role, entry_data in kit.items():
            if isinstance(entry_data, dict):
                kit_entries[role] = SampleEntry.from_dict(entry_data)
            else:
                # Assume it's just a path string
                kit_entries[role] = SampleEntry(base=str(entry_data), variations={})
        
        # Create metadata object
        kit_metadata = KitMetadata.from_dict(metadata)
        kit_metadata.coherence_score = coherence_score
        
        # Check for duplicates
        if not allow_duplicates:
            duplicate_name = self._check_duplicate(kit_entries)
            if duplicate_name:
                return (False, f"Duplicate of existing preset: '{duplicate_name}'", None)
        
        # Generate name if not provided
        if not name:
            name = self._generate_name(kit_metadata, kit_entries)
        
        # Generate description
        description = self._generate_description(kit_metadata, kit_entries)
        
        # Create coherence proof
        if coherence_proof is None:
            coherence_proof = {
                "overall_score": coherence_score,
                "pair_scores": []
            }
        
        proof = CoherenceProof.from_dict(coherence_proof)
        
        # Create preset
        preset = Preset(
            name=name,
            description=description,
            created_at=datetime.now().isoformat(),
            metadata=kit_metadata,
            kit=kit_entries,
            coherence_proof=proof,
            usage_count=0,
            last_used=""
        )
        
        # Generate filename
        filename = self._generate_filename(kit_metadata)
        filepath = self.presets_dir / filename
        
        # Save to file
        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                json.dump(preset.to_dict(), f, indent=2, ensure_ascii=False)
            
            # Update cache
            self._cache[name] = preset
            
            return (True, f"Saved preset '{name}' to {filename}", preset)
        except Exception as e:
            return (False, f"Failed to save preset: {str(e)}", None)
    
    def load_preset(self, name: str) -> Tuple[bool, str, Optional[Preset]]:
        """
        Load a preset by name.
        
        Args:
            name: Preset name to load
        
        Returns:
            Tuple of (success: bool, message: str, preset: Optional[Preset])
        """
        # Check cache first
        if name in self._cache:
            return (True, "Loaded from cache", self._cache[name])
        
        # Search files
        for filename in self.presets_dir.glob("*.json"):
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    if data.get("name") == name:
                        preset = Preset.from_dict(data)
                        self._cache[name] = preset
                        return (True, f"Loaded from {filename.name}", preset)
            except Exception as e:
                continue
        
        return (False, f"Preset '{name}' not found", None)
    
    def list_presets(
        self,
        genre: Optional[str] = None,
        style: Optional[str] = None,
        min_coherence: float = 0.0,
        max_coherence: float = 1.0,
        tags: Optional[List[str]] = None,
        sort_by: str = "coherence",  # "coherence", "usage", "date", "name"
        limit: int = 100
    ) -> List[Preset]:
        """
        List presets with filtering and sorting.
        
        Args:
            genre: Filter by genre
            style: Filter by style
            min_coherence: Minimum coherence score
            max_coherence: Maximum coherence score
            tags: Filter by tags (all must match)
            sort_by: Sort field ("coherence", "usage", "date", "name")
            limit: Maximum results to return
        
        Returns:
            List of matching Preset objects
        """
        presets = []
        
        for filename in self.presets_dir.glob("*.json"):
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    preset = Preset.from_dict(data)
                    
                    # Apply filters
                    if genre and preset.metadata.genre.lower() != genre.lower():
                        continue
                    
                    if style and preset.metadata.style.lower() != style.lower():
                        continue
                    
                    if preset.metadata.coherence_score < min_coherence:
                        continue
                    
                    if preset.metadata.coherence_score > max_coherence:
                        continue
                    
                    if tags:
                        preset_tags = set(t.lower() for t in preset.metadata.tags)
                        if not all(t.lower() in preset_tags for t in tags):
                            continue
                    
                    presets.append(preset)
            except:
                pass
        
        # Sort
        if sort_by == "coherence":
            presets.sort(key=lambda p: p.metadata.coherence_score, reverse=True)
        elif sort_by == "usage":
            presets.sort(key=lambda p: p.usage_count, reverse=True)
        elif sort_by == "date":
            presets.sort(key=lambda p: p.created_at, reverse=True)
        elif sort_by == "name":
            presets.sort(key=lambda p: p.name.lower())
        
        return presets[:limit]
    
    def find_similar_presets(
        self,
        reference_kit: Dict[str, Any],
        count: int = 5,
        min_coherence: float = 0.85
    ) -> List[Tuple[Preset, float]]:
        """
        Find presets similar to a reference kit.
        
        Args:
            reference_kit: Dictionary of role -> sample paths
            count: Number of results to return
            min_coherence: Minimum coherence for candidates
        
        Returns:
            List of (preset, similarity_score) tuples
        """
        # Get all presets above minimum coherence
        candidates = self.list_presets(min_coherence=min_coherence)
        
        if not candidates:
            return []
        
        # Calculate similarity scores
        scored_presets = []
        
        for preset in candidates:
            score = self._calculate_similarity(reference_kit, preset)
            scored_presets.append((preset, score))
        
        # Sort by score
        scored_presets.sort(key=lambda x: x[1], reverse=True)
        
        return scored_presets[:count]
    
    def _calculate_similarity(
        self,
        reference_kit: Dict[str, Any],
        preset: Preset
    ) -> float:
        """
        Calculate similarity between reference kit and preset.
        
        Based on:
        - Role overlap (same roles present)
        - Sample path similarity (same pack, similar names)
        - Metadata match (tempo, key)
        """
        scores = []
        
        # Role overlap
        ref_roles = set(reference_kit.keys())
        preset_roles = set(preset.kit.keys())
        
        if ref_roles and preset_roles:
            intersection = len(ref_roles & preset_roles)
            union = len(ref_roles | preset_roles)
            role_score = intersection / union if union > 0 else 0
            scores.append(role_score)
        
        # Sample name similarity for matching roles
        name_scores = []
        for role in ref_roles & preset_roles:
            ref_entry = reference_kit[role]
            if isinstance(ref_entry, dict):
                ref_path = ref_entry.get("base", "")
            else:
                ref_path = str(ref_entry)
            
            preset_path = preset.kit[role].base
            
            # Extract filenames
            ref_name = os.path.basename(ref_path).lower().replace(".wav", "")
            preset_name = os.path.basename(preset_path).lower().replace(".wav", "")
            
            # Check for common words
            ref_words = set(ref_name.split("_"))
            preset_words = set(preset_name.split("_"))
            
            if ref_words and preset_words:
                common = len(ref_words & preset_words)
                total = len(ref_words | preset_words)
                name_scores.append(common / total if total > 0 else 0)
        
        if name_scores:
            scores.append(sum(name_scores) / len(name_scores))
        
        # Combine scores
        return sum(scores) / len(scores) if scores else 0.0
    
    def delete_preset(self, name: str) -> Tuple[bool, str]:
        """
        Delete a preset by name.
        
        Args:
            name: Preset name to delete
        
        Returns:
            Tuple of (success: bool, message: str)
        """
        # Find file
        for filename in self.presets_dir.glob("*.json"):
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    if data.get("name") == name:
                        # Delete file
                        filename.unlink()
                        
                        # Remove from cache
                        if name in self._cache:
                            del self._cache[name]
                        
                        return (True, f"Deleted preset '{name}'")
            except:
                pass
        
        return (False, f"Preset '{name}' not found")
    
    def increment_usage(self, name: str) -> Tuple[bool, str]:
        """
        Increment usage counter for a preset.
        
        Args:
            name: Preset name
        
        Returns:
            Tuple of (success: bool, message: str)
        """
        success, msg, preset = self.load_preset(name)
        
        if not success or preset is None:
            return (False, msg)
        
        # Update usage
        preset.usage_count += 1
        preset.last_used = datetime.now().isoformat()
        
        # Find and update file
        for filename in self.presets_dir.glob("*.json"):
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    if data.get("name") == name:
                        # Update and save
                        data["usage_count"] = preset.usage_count
                        data["last_used"] = preset.last_used
                        
                        with open(filename, 'w', encoding='utf-8') as f:
                            json.dump(data, f, indent=2, ensure_ascii=False)
                        
                        # Update cache
                        self._cache[name] = preset
                        
                        return (True, f"Usage count: {preset.usage_count}")
            except:
                pass
        
        return (False, "Failed to update usage count")
    
    def export_preset(self, name: str, path: str) -> Tuple[bool, str]:
        """
        Export a preset to an external location for sharing.
        
        Args:
            name: Preset name to export
            path: Destination path
        
        Returns:
            Tuple of (success: bool, message: str)
        """
        success, msg, preset = self.load_preset(name)
        
        if not success or preset is None:
            return (False, msg)
        
        try:
            dest_path = Path(path)
            
            # Create directory if needed
            dest_path.parent.mkdir(parents=True, exist_ok=True)
            
            # Export as JSON
            with open(dest_path, 'w', encoding='utf-8') as f:
                json.dump(preset.to_dict(), f, indent=2, ensure_ascii=False)
            
            return (True, f"Exported to {dest_path}")
        except Exception as e:
            return (False, f"Export failed: {str(e)}")
    
    def import_preset(self, path: str, allow_overwrite: bool = False) -> Tuple[bool, str, Optional[Preset]]:
        """
        Import a preset from an external file.
        
        Args:
            path: Path to external preset JSON
            allow_overwrite: If True, overwrites existing preset with same name
        
        Returns:
            Tuple of (success: bool, message: str, preset: Optional[Preset])
        """
        try:
            source_path = Path(path)
            
            if not source_path.exists():
                return (False, f"File not found: {path}", None)
            
            # Load preset data
            with open(source_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            preset = Preset.from_dict(data)
            
            # Check for existing
            existing = self.load_preset(preset.name)
            if existing[0] and not allow_overwrite:
                return (False, f"Preset '{preset.name}' already exists (use allow_overwrite=True)", None)
            
            # Generate new filename
            filename = self._generate_filename(preset.metadata)
            dest_path = self.presets_dir / filename
            
            # Copy file
            shutil.copy2(source_path, dest_path)
            
            # Update cache
            self._cache[preset.name] = preset
            
            return (True, f"Imported preset '{preset.name}'", preset)
            
        except Exception as e:
            return (False, f"Import failed: {str(e)}", None)
    
    def get_preset_stats(self) -> Dict[str, Any]:
        """
        Get statistics about stored presets.
        
        Returns:
            Dictionary with statistics
        """
        presets = self.list_presets(limit=10000)
        
        if not presets:
            return {
                "total_presets": 0,
                "avg_coherence": 0.0,
                "genres": {},
                "styles": {},
                "most_used": None
            }
        
        # Calculate stats
        coherence_scores = [p.metadata.coherence_score for p in presets]
        
        genres = {}
        styles = {}
        for p in presets:
            genres[p.metadata.genre] = genres.get(p.metadata.genre, 0) + 1
            styles[p.metadata.style] = styles.get(p.metadata.style, 0) + 1
        
        most_used = max(presets, key=lambda p: p.usage_count)
        
        return {
            "total_presets": len(presets),
            "avg_coherence": sum(coherence_scores) / len(coherence_scores),
            "min_coherence": min(coherence_scores),
            "max_coherence": max(coherence_scores),
            "genres": genres,
            "styles": styles,
            "most_used": {
                "name": most_used.name,
                "usage_count": most_used.usage_count
            } if most_used.usage_count > 0 else None
        }
    
    def clear_cache(self):
        """Clear the preset cache."""
        self._cache.clear()
        self._cache_timestamp = None


# Convenience functions for direct usage
def get_preset_manager() -> PresetManager:
    """Get default PresetManager instance."""
    return PresetManager()


# Example usage
if __name__ == "__main__":
    # Create manager
    manager = PresetManager()
    
    # Example kit
    example_kit = {
        "kick": {
            "base": "/path/to/Kick_Pesado_01.wav",
            "variations": {
                "intro": "/path/to/Kick_Sutil_12.wav",
                "verse": "/path/to/Kick_Estampido_07.wav",
                "chorus": "/path/to/Kick_Agresivo_03.wav"
            }
        },
        "snare": {
            "base": "/path/to/Snare_Corte_01.wav",
            "variations": {}
        },
        "bass": {
            "base": "/path/to/Bass_Profundo_02.wav",
            "variations": {}
        }
    }
    
    # Example metadata
    metadata = {
        "genre": "reggaeton",
        "style": "perreo_intenso",
        "tempo": 95,
        "key": "Am",
        "variation_level": "high",
        "tags": ["heavy", "energetic"]
    }
    
    # Save preset
    success, msg, preset = manager.save_preset(
        name=None,  # Auto-generate
        kit=example_kit,
        coherence_score=0.91,
        metadata=metadata
    )
    
    print(f"Save: {success} - {msg}")
    
    # List presets
    presets = manager.list_presets(sort_by="coherence")
    print(f"\nFound {len(presets)} presets:")
    for p in presets:
        print(f"  - {p.name} ({p.metadata.coherence_score:.2f})")
    
    # Stats
    stats = manager.get_preset_stats()
    print(f"\nStats: {stats}")
