"""
Agente 9: Device Parameter Discovery System

Provides intelligent parameter discovery and fuzzy matching for Ableton Live devices.
Enables automatic parameter identification across different Live versions and device types.

Key Features:
- Enumerate all parameters for a device on a track
- Fuzzy matching for parameter names (handles variations like "Attack" vs "Atk")
- Caching by Live version for performance
- Fallback handling for missing parameters
- Integration with LiveBridge for automatic discovery
"""

import json
import os
import re
import logging
from typing import Dict, List, Optional, Any, Tuple
from difflib import SequenceMatcher
from dataclasses import dataclass, field
from pathlib import Path

logger = logging.getLogger("ParameterDiscovery")


@dataclass
class DeviceParameter:
    """Represents a single device parameter with metadata."""
    name: str
    index: int
    min_value: float = 0.0
    max_value: float = 1.0
    default_value: float = 0.0
    is_enabled: bool = True
    automation_state: str = "none"  # "none", "enabled", "playing"
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert parameter to dictionary."""
        return {
            "name": self.name,
            "index": self.index,
            "min_value": self.min_value,
            "max_value": self.max_value,
            "default_value": self.default_value,
            "is_enabled": self.is_enabled,
            "automation_state": self.automation_state,
        }


@dataclass
class DeviceInfo:
    """Complete information about a device and its parameters."""
    name: str
    device_index: int
    track_index: int
    class_name: str = ""
    parameters: List[DeviceParameter] = field(default_factory=list)
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert device info to dictionary."""
        return {
            "name": self.name,
            "device_index": self.device_index,
            "track_index": self.track_index,
            "class_name": self.class_name,
            "parameter_count": len(self.parameters),
            "parameters": [p.to_dict() for p in self.parameters],
        }


class ParameterDiscovery:
    """
    Agent 9: Device Parameter Discovery System
    
    Discovers and manages Ableton Live device parameters with:
    - Automatic enumeration of device parameters
    - Fuzzy name matching for parameter lookup
    - Version-specific caching
    - Fallback strategies for missing parameters
    
    Usage:
        discovery = ParameterDiscovery()
        
        # Enumerate parameters
        params = discovery.enumerate_device_parameters(track_index=0, device_index=0)
        
        # Fuzzy match a parameter name
        match = discovery.fuzzy_match_parameter("attack", [p.name for p in params])
        
        # Get cached version
        discovery.cache_parameters_by_live_version("12.0.1")
    """
    
    # Common parameter name aliases for fuzzy matching
    PARAMETER_ALIASES = {
        # Timing
        "attack": ["attack", "atk", "att", "a"],
        "release": ["release", "rel", "r", "rls"],
        "decay": ["decay", "dec", "d"],
        "sustain": ["sustain", "sus", "s"],
        "hold": ["hold", "h"],
        # Frequency
        "frequency": ["frequency", "freq", "frq", "cutoff", "cut"],
        "cutoff": ["cutoff", "cut", "frequency", "freq"],
        "resonance": ["resonance", "res", "q", "peak"],
        "bandwidth": ["bandwidth", "bw", "band"],
        # Dynamics
        "threshold": ["threshold", "thresh", "thr", "th"],
        "ratio": ["ratio", "rat", "rto"],
        "makeup": ["makeup", "gain", "make up", "mgain"],
        "knee": ["knee", "k"],
        # Levels
        "gain": ["gain", "volume", "vol", "level", "amp", "amplitude"],
        "volume": ["volume", "vol", "gain", "level"],
        "drive": ["drive", "drv", "dist", "distortion"],
        "mix": ["mix", "blend", "wet", "dry/wet"],
        "wet": ["wet", "wet level", "mix"],
        "dry": ["dry", "dry level", "bypass"],
        # Effects
        "feedback": ["feedback", "fb", "feed"],
        "time": ["time", "t", "delay", "del"],
        "rate": ["rate", "speed", "spd", "freq"],
        "depth": ["depth", "d", "amt", "amount"],
        "spread": ["spread", "sprd", "width", "wide"],
        "size": ["size", "sz", "room", "space"],
        "damping": ["damping", "damp", "dmp"],
        "stiffness": ["stiffness", "stiff", "tension"],
        # EQ specific
        "low": ["low", "bass", "lo", "lowshelf"],
        "mid": ["mid", "middle", "mids", "midrange"],
        "high": ["high", "treble", "hi", "highshelf", "highpass"],
        "lowshelf": ["lowshelf", "low shelf", "bass"],
        "highshelf": ["highshelf", "high shelf", "treble"],
        "lowcut": ["lowcut", "highpass", "hpf"],
        "highcut": ["highcut", "lowpass", "lpf"],
        # Sidechain
        "sidechain": ["sidechain", "sc", "side chain"],
        "sidechain_input": ["sidechain input", "sc input", "external"],
        # Generic
        "bypass": ["bypass", "byp", "on/off", "enabled"],
        "on": ["on", "enabled", "active", "bypass"],
        "type": ["type", "mode", "style", "circuit"],
    }
    
    def __init__(self, cache_dir: Optional[str] = None):
        """
        Initialize the ParameterDiscovery system.
        
        Args:
            cache_dir: Directory for caching parameter definitions by Live version.
                      If None, uses a default location.
        """
        self.cache_dir = cache_dir or self._get_default_cache_dir()
        self._version_cache: Dict[str, Dict[str, Any]] = {}
        self._live_bridge = None
        
        # Ensure cache directory exists
        os.makedirs(self.cache_dir, exist_ok=True)
        
        logger.info(f"ParameterDiscovery initialized with cache: {self.cache_dir}")
    
    def _get_default_cache_dir(self) -> str:
        """Get the default cache directory path."""
        script_dir = Path(__file__).resolve().parent
        return str(script_dir.parent.parent / "cache" / "parameter_discovery")
    
    def set_live_bridge(self, bridge):
        """
        Set the LiveBridge for direct Ableton API access.
        
        Args:
            bridge: AbletonLiveBridge instance
        """
        self._live_bridge = bridge
        logger.info("LiveBridge connected for automatic parameter discovery")
    
    def enumerate_device_parameters(
        self,
        track_index: int,
        device_index: int,
        song=None
    ) -> List[DeviceParameter]:
        """
        Enumerate all parameters for a device on a track.
        
        Args:
            track_index: Index of the track containing the device
            device_index: Index of the device on the track
            song: Optional Live Song object (if not using LiveBridge)
            
        Returns:
            List of DeviceParameter objects
            
        Raises:
            IndexError: If track or device index is out of range
            Exception: If device access fails
        """
        parameters = []
        
        try:
            # Try using LiveBridge first if available
            if self._live_bridge and self._live_bridge.song:
                song = self._live_bridge.song
            
            if song is None:
                raise Exception("No song object available (LiveBridge not set or no song provided)")
            
            # Validate track index
            if track_index < 0 or track_index >= len(song.tracks):
                raise IndexError(f"Track index {track_index} out of range (0-{len(song.tracks)-1})")
            
            track = song.tracks[track_index]
            
            # Validate device index
            if not hasattr(track, 'devices'):
                raise Exception(f"Track {track_index} has no devices attribute")
            
            if device_index < 0 or device_index >= len(track.devices):
                raise IndexError(
                    f"Device index {device_index} out of range (0-{len(track.devices)-1}) on track {track_index}"
                )
            
            device = track.devices[device_index]
            
            # Enumerate parameters
            if hasattr(device, 'parameters'):
                for idx, param in enumerate(device.parameters):
                    try:
                        # Extract parameter properties safely
                        param_name = str(getattr(param, 'name', f'Parameter_{idx}'))
                        min_val = float(getattr(param, 'min', 0.0)) if hasattr(param, 'min') else 0.0
                        max_val = float(getattr(param, 'max', 1.0)) if hasattr(param, 'max') else 1.0
                        
                        # Get current value (use as default)
                        default_val = 0.0
                        if hasattr(param, 'value'):
                            try:
                                default_val = float(param.value)
                            except:
                                pass
                        
                        # Check if enabled
                        is_enabled = True
                        if hasattr(param, 'is_enabled'):
                            is_enabled = bool(param.is_enabled)
                        
                        # Get automation state
                        automation = "none"
                        if hasattr(param, 'automation_state'):
                            automation = str(param.automation_state)
                        
                        device_param = DeviceParameter(
                            name=param_name,
                            index=idx,
                            min_value=min_val,
                            max_value=max_val,
                            default_value=default_val,
                            is_enabled=is_enabled,
                            automation_state=automation
                        )
                        parameters.append(device_param)
                        
                    except Exception as e:
                        logger.warning(f"Error reading parameter {idx}: {e}")
                        # Add placeholder parameter
                        parameters.append(DeviceParameter(
                            name=f"Parameter_{idx}",
                            index=idx,
                            is_enabled=False
                        ))
            
            logger.info(f"Discovered {len(parameters)} parameters for device {device_index} on track {track_index}")
            return parameters
            
        except IndexError:
            raise
        except Exception as e:
            logger.error(f"Failed to enumerate parameters: {e}")
            raise
    
    def fuzzy_match_parameter(
        self,
        target_name: str,
        available_params: List[str],
        threshold: float = 0.6
    ) -> Optional[Tuple[str, float]]:
        """
        Fuzzy match a parameter name against available parameters.
        
        Uses a combination of:
        1. Exact matching (case-insensitive)
        2. Alias matching (handles common synonyms like "atk" for "attack")
        3. Similarity scoring (SequenceMatcher for fuzzy matching)
        
        Args:
            target_name: The parameter name to search for
            available_params: List of available parameter names
            threshold: Minimum similarity score (0.0-1.0) for a match
            
        Returns:
            Tuple of (matched_name, score) or None if no match found
            
        Example:
            >>> discovery.fuzzy_match_parameter("atk", ["Attack", "Release", "Threshold"])
            ("Attack", 0.8)
        """
        if not available_params:
            return None
        
        target_lower = target_name.lower().strip()
        
        # 1. Exact match (case-insensitive)
        for param in available_params:
            if param.lower() == target_lower:
                return (param, 1.0)
        
        # 2. Check parameter aliases
        for canonical, aliases in self.PARAMETER_ALIASES.items():
            if target_lower in aliases or target_lower == canonical.lower():
                # Find best match in available params
                for param in available_params:
                    param_lower = param.lower()
                    if param_lower in aliases or param_lower == canonical.lower():
                        return (param, 0.95)
        
        # 3. Fuzzy matching with similarity score
        best_match = None
        best_score = 0.0
        
        for param in available_params:
            # Use SequenceMatcher for fuzzy string matching
            similarity = SequenceMatcher(None, target_lower, param.lower()).ratio()
            
            # Boost score for partial word matches
            if target_lower in param.lower() or param.lower() in target_lower:
                similarity = max(similarity, 0.8)
            
            if similarity > best_score and similarity >= threshold:
                best_score = similarity
                best_match = param
        
        if best_match:
            return (best_match, round(best_score, 3))
        
        return None
    
    def find_parameter(
        self,
        target_name: str,
        parameters: List[DeviceParameter],
        threshold: float = 0.6
    ) -> Optional[DeviceParameter]:
        """
        Find a parameter by name from a list of DeviceParameter objects.
        
        Args:
            target_name: Parameter name to search for
            parameters: List of DeviceParameter objects
            threshold: Minimum similarity score
            
        Returns:
            Matching DeviceParameter or None
        """
        available_names = [p.name for p in parameters]
        match = self.fuzzy_match_parameter(target_name, available_names, threshold)
        
        if match:
            matched_name, _ = match
            for param in parameters:
                if param.name == matched_name:
                    return param
        
        return None
    
    def cache_parameters_by_live_version(self, version: str) -> Dict[str, Any]:
        """
        Cache parameter definitions for a specific Live version.
        
        This stores common device parameter layouts for quick lookup
        without needing to query Live's API each time.
        
        Args:
            version: Ableton Live version string (e.g., "12.0.1")
            
        Returns:
            Dictionary of cached parameter definitions
        """
        cache_file = os.path.join(self.cache_dir, f"params_v{version}.json")
        
        # Try to load existing cache
        if os.path.exists(cache_file):
            try:
                with open(cache_file, 'r') as f:
                    cached = json.load(f)
                self._version_cache[version] = cached
                logger.info(f"Loaded cached parameters for Live {version}")
                return cached
            except Exception as e:
                logger.warning(f"Failed to load cache: {e}")
        
        # Create default cache for common devices
        default_cache = {
            "version": version,
            "devices": {
                "EQ Eight": {
                    "parameters": [
                        "Gain Low", "Freq Low", "Gain Mid 1", "Freq Mid 1", "Q Mid 1",
                        "Gain Mid 2", "Freq Mid 2", "Q Mid 2", "Gain Mid 3", "Freq Mid 3", "Q Mid 3",
                        "Gain High", "Freq High", "Scale", "Output Gain"
                    ]
                },
                "Compressor": {
                    "parameters": [
                        "Threshold", "Ratio", "Attack", "Release", "Makeup",
                        "Knee", "Model", "Sidechain", "Sidechain Listen"
                    ]
                },
                "Reverb": {
                    "parameters": [
                        "Predelay", "Decay", "Size", "Damping", "Diffusion",
                        "Dry/Wet", "Reflect", "Shape"
                    ]
                },
                "Auto Filter": {
                    "parameters": [
                        "Frequency", "Resonance", "LFO Amount", "LFO Rate",
                        "LFO Shape", "Phase", "Envelope Amount"
                    ]
                },
                "Saturator": {
                    "parameters": [
                        "Drive", "Base", "Frequency", "Width", "Depth",
                        "Output", "Color", "Mode"
                    ]
                },
                "Delay": {
                    "parameters": [
                        "Time", "Feedback", "Dry/Wet", "Offset", "Sync"
                    ]
                },
                "Limiter": {
                    "parameters": [
                        "Gain", "Ceiling", "Release", "Lookahead"
                    ]
                },
            }
        }
        
        # Save cache
        try:
            with open(cache_file, 'w') as f:
                json.dump(default_cache, f, indent=2)
            self._version_cache[version] = default_cache
            logger.info(f"Created parameter cache for Live {version}")
        except Exception as e:
            logger.error(f"Failed to save cache: {e}")
        
        return default_cache
    
    def get_cached_device_params(self, version: str, device_name: str) -> Optional[List[str]]:
        """
        Get cached parameter names for a device type.
        
        Args:
            version: Live version string
            device_name: Name of the device
            
        Returns:
            List of parameter names if cached, None otherwise
        """
        if version not in self._version_cache:
            self.cache_parameters_by_live_version(version)
        
        cache = self._version_cache.get(version, {})
        devices = cache.get("devices", {})
        
        # Try exact match first
        if device_name in devices:
            return devices[device_name].get("parameters", [])
        
        # Try fuzzy match on device names
        for cached_name, info in devices.items():
            if device_name.lower() in cached_name.lower() or cached_name.lower() in device_name.lower():
                return info.get("parameters", [])
        
        return None
    
    def clear_cache(self):
        """Clear all cached parameter definitions."""
        self._version_cache.clear()
        try:
            import shutil
            if os.path.exists(self.cache_dir):
                shutil.rmtree(self.cache_dir)
                os.makedirs(self.cache_dir, exist_ok=True)
            logger.info("Parameter cache cleared")
        except Exception as e:
            logger.error(f"Failed to clear cache: {e}")
    
    def get_parameter_suggestions(
        self,
        partial_name: str,
        available_params: List[str],
        max_suggestions: int = 5
    ) -> List[Tuple[str, float]]:
        """
        Get parameter name suggestions based on partial input.
        
        Args:
            partial_name: Partial parameter name to search for
            available_params: List of available parameter names
            max_suggestions: Maximum number of suggestions to return
            
        Returns:
            List of (param_name, score) tuples sorted by relevance
        """
        scores = []
        partial_lower = partial_name.lower()
        
        for param in available_params:
            param_lower = param.lower()
            
            # Calculate relevance score
            if partial_lower == param_lower:
                score = 1.0  # Exact match
            elif param_lower.startswith(partial_lower):
                score = 0.9  # Starts with
            elif partial_lower in param_lower:
                score = 0.7  # Contains
            else:
                # Fuzzy match
                score = SequenceMatcher(None, partial_lower, param_lower).ratio()
            
            scores.append((param, score))
        
        # Sort by score descending and return top suggestions
        scores.sort(key=lambda x: x[1], reverse=True)
        return scores[:max_suggestions]
    
    def handle_missing_parameter(
        self,
        target_name: str,
        parameters: List[DeviceParameter],
        fallback_strategy: str = "fuzzy"
    ) -> Dict[str, Any]:
        """
        Handle the case when a parameter is not found.
        
        Args:
            target_name: The parameter that was not found
            parameters: Available parameters
            fallback_strategy: Strategy to use ("fuzzy", "suggest", "error")
            
        Returns:
            Dictionary with fallback information
        """
        available_names = [p.name for p in parameters]
        
        result = {
            "target": target_name,
            "found": False,
            "available_parameters": available_names,
            "strategy": fallback_strategy,
        }
        
        if fallback_strategy == "fuzzy":
            # Try fuzzy matching
            match = self.fuzzy_match_parameter(target_name, available_names, threshold=0.5)
            if match:
                matched_name, score = match
                result["fallback"] = {
                    "type": "fuzzy_match",
                    "suggested_parameter": matched_name,
                    "confidence": score,
                }
                result["found"] = True
                result["matched_parameter"] = matched_name
        
        elif fallback_strategy == "suggest":
            # Get suggestions
            suggestions = self.get_parameter_suggestions(target_name, available_names, max_suggestions=5)
            result["fallback"] = {
                "type": "suggestions",
                "suggestions": [{"name": name, "score": score} for name, score in suggestions],
            }
        
        elif fallback_strategy == "error":
            result["fallback"] = {
                "type": "error",
                "message": f"Parameter '{target_name}' not found in device",
            }
        
        return result


# Convenience functions for direct use
def discover_parameters(track_index: int, device_index: int, song=None) -> List[Dict[str, Any]]:
    """
    Convenience function to discover parameters without instantiating the class.
    
    Args:
        track_index: Track index
        device_index: Device index
        song: Live Song object
        
    Returns:
        List of parameter dictionaries
    """
    discovery = ParameterDiscovery()
    params = discovery.enumerate_device_parameters(track_index, device_index, song)
    return [p.to_dict() for p in params]


def match_parameter(target: str, available: List[str]) -> Optional[Tuple[str, float]]:
    """
    Convenience function for fuzzy parameter matching.
    
    Args:
        target: Target parameter name
        available: List of available parameter names
        
    Returns:
        Tuple of (matched_name, score) or None
    """
    discovery = ParameterDiscovery()
    return discovery.fuzzy_match_parameter(target, available)


# Export the main class and convenience functions
__all__ = [
    "ParameterDiscovery",
    "DeviceParameter",
    "DeviceInfo",
    "discover_parameters",
    "match_parameter",
]
