from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Dict, List, Optional

import reapy
from reapy import reascript_api as RPR

from reaper_mcp.core.mcp_core import mcp
from reaper_mcp.core.util import _load_sample_dirs, _save_sample_dirs, _load_sample_metadata, is_cache_valid

logger = logging.getLogger(__name__)


@mcp.tool()
def list_sample_dirs() -> Dict[str, Any]:
    """List configured sample directories."""
    return {"sample_dirs": _load_sample_dirs()}


@mcp.tool()
def add_sample_dir(path: str) -> Dict[str, Any]:
    """Add a sample directory (persisted)."""
    if not path:
        return {"error": "Path is required"}
    dirs = _load_sample_dirs()
    if path not in dirs:
        dirs.append(path)
        _save_sample_dirs(dirs)
    return {"sample_dirs": dirs}


@mcp.tool()
def remove_sample_dir(path: str) -> Dict[str, Any]:
    """Remove a sample directory."""
    dirs = [d for d in _load_sample_dirs() if d != path]
    _save_sample_dirs(dirs)
    return {"sample_dirs": dirs}


@mcp.tool()
def search_samples(query: Optional[str] = None, exts: Optional[List[str]] = None, limit: int = 100) -> Dict[str, Any]:
    """Search for audio samples across configured directories.

    query: substring to match in file names (case-insensitive)
    exts: list of extensions to include (default: wav,aiff,flac,mp3,ogg)
    limit: maximum results
    """
    dirs = _load_sample_dirs()
    if not dirs:
        return {"error": "No sample directories configured."}
    if not exts:
        exts = [".wav", ".aiff", ".aif", ".flac", ".mp3", ".ogg"]
    q = (query or "").lower()
    results: List[str] = []
    try:
        for d in dirs:
            base = Path(d)
            for file_path in base.rglob('*'):
                if file_path.is_file() and any(file_path.name.lower().endswith(e) for e in exts):
                    if not q or q in file_path.name.lower():
                        results.append(str(file_path))
                        if len(results) >= limit:
                            return {"files": results}
        return {"files": results}
    except Exception as e:
        return {"error": f"Failed to search samples: {e}"}


@mcp.tool()
def import_sample_to_track(track_index: int, file_path: str, insert_time: float = 0.0, time_stretch_playrate: Optional[float] = None) -> Dict[str, Any]:
    """Import a sample (audio or MIDI file) onto the given track at time position. Optionally set take playrate for time-stretching.

    Args:
        track_index: Track index (0-based) to import sample to
        file_path: Full path to the audio or MIDI file to import
        insert_time: Time position in seconds to insert the sample
        time_stretch_playrate: If provided, sets the active take's playback rate (1.0 = no stretch, 2.0 = double speed)
    
    Note: If you receive a 422 error, ensure numeric parameters (track_index, insert_time, time_stretch_playrate)
          are sent as numbers, not strings.
    """
    logger.info(f"import_sample_to_track called with track_index={track_index}, file_path={file_path}, "
                f"insert_time={insert_time}, time_stretch_playrate={time_stretch_playrate}")
    if not Path(file_path).is_file():
        error_msg = f"File not found: {file_path}"
        logger.warning(error_msg)
        return {"error": error_msg}
    try:
        project = reapy.Project()
        tracks = list(project.tracks)
        if track_index < 0 or track_index >= len(tracks):
            error_msg = f"Track index out of range: {track_index} (valid: 0-{len(tracks)-1})"
            logger.warning(error_msg)
            return {"error": error_msg}
        track = tracks[track_index]
        
        # Use REAPER's InsertMedia API to import the file
        # First, unselect all tracks and select only the target track
        for t in tracks:
            t.is_selected = False
        track.is_selected = True
        
        # Set edit cursor to insert position
        project.cursor_position = float(insert_time)
        
        # Insert media at cursor position on selected track (mode 0)
        RPR.InsertMedia(file_path, 0)
        
        # Get the newly created item (it should be at the cursor position on this track)
        item = None
        for track_item in track.items:
            if abs(track_item.position - float(insert_time)) < 0.001:  # Small tolerance for floating point
                item = track_item
                break
        
        if item and time_stretch_playrate is not None:
            take = item.active_take
            if take:
                take.playback_rate = float(time_stretch_playrate)
        
        logger.info(f"Successfully imported sample to track {track_index} at time {insert_time}")
        return {"ok": True}
    except Exception as e:
        error_msg = f"Failed to import sample: {e}"
        logger.error(error_msg, exc_info=True)
        return {"error": error_msg}


@mcp.tool()
def get_sample_metadata(file_path: str) -> Dict[str, Any]:
    """Get cached metadata for a sample file, or trigger analysis if not cached.
    
    Returns cached analysis results if available and valid, otherwise triggers
    on-demand analysis. This is useful for quickly retrieving metadata without
    re-analyzing files.
    
    Args:
        file_path: Full path to the audio file
        
    Returns:
        Dictionary containing sample metadata (BPM, key, duration, etc.)
    """
    try:
        if not Path(file_path).is_file():
            return {"error": f"File not found: {file_path}"}
        
        # Check cache
        cache = _load_sample_metadata()
        cached_data = cache.get(file_path)
        
        if cached_data and is_cache_valid(file_path, cached_data.get("mtime")):
            logger.info(f"Returning cached metadata for {file_path}")
            return {"metadata": cached_data, "cached": True}
        
        # Not cached or invalid - need to analyze
        # Import here to avoid circular dependency
        from reaper_mcp.audio_analysis import analyze_audio_file
        
        logger.info(f"No valid cache for {file_path}, triggering analysis")
        result = analyze_audio_file(file_path)
        
        if "analysis" in result:
            return {"metadata": result["analysis"], "cached": False}
        else:
            return result  # Return error if analysis failed
            
    except Exception as e:
        error_msg = f"Failed to get sample metadata: {e}"
        logger.error(error_msg, exc_info=True)
        return {"error": error_msg}


@mcp.tool()
def search_samples_by_attributes(
    bpm_min: Optional[float] = None,
    bpm_max: Optional[float] = None,
    key: Optional[str] = None,
    duration_min: Optional[float] = None,
    duration_max: Optional[float] = None,
    min_rms_energy: Optional[float] = None,
    limit: int = 50
) -> Dict[str, Any]:
    """Search for samples by musical and audio attributes using cached metadata.
    
    Filters samples based on tempo (BPM), key, duration, and RMS energy.
    Only searches through previously analyzed samples (those in the metadata cache).
    Use batch_analyze_samples first to build the metadata cache.
    
    Args:
        bpm_min: Minimum tempo in BPM (optional)
        bpm_max: Maximum tempo in BPM (optional)
        key: Musical key to match (e.g., 'C', 'D#', 'F') (optional)
        duration_min: Minimum duration in seconds (optional)
        duration_max: Maximum duration in seconds (optional)
        min_rms_energy: Minimum RMS energy level (optional)
        limit: Maximum number of results to return (default: 50)
        
    Returns:
        Dictionary with matching samples and their metadata
    """
    try:
        cache = _load_sample_metadata()
        
        if not cache:
            return {
                "error": "No sample metadata cache found. Use batch_analyze_samples to analyze samples first.",
                "matches": []
            }
        
        matches = []
        
        for file_path, metadata in cache.items():
            # Validate cache entry
            if not is_cache_valid(file_path, metadata.get("mtime")):
                continue
            
            # Apply filters
            if bpm_min is not None and metadata.get("estimated_tempo_bpm", 0) < bpm_min:
                continue
            if bpm_max is not None and metadata.get("estimated_tempo_bpm", 999) > bpm_max:
                continue
            if key is not None and metadata.get("estimated_key", "").upper() != key.upper():
                continue
            if duration_min is not None and metadata.get("duration", 0) < duration_min:
                continue
            if duration_max is not None and metadata.get("duration", 999999) > duration_max:
                continue
            if min_rms_energy is not None and metadata.get("rms_energy", 0) < min_rms_energy:
                continue
            
            matches.append(metadata)
            
            if len(matches) >= limit:
                break
        
        logger.info(f"Found {len(matches)} samples matching criteria")
        return {
            "matches": matches,
            "count": len(matches),
            "filters_applied": {
                "bpm_range": [bpm_min, bpm_max] if bpm_min or bpm_max else None,
                "key": key,
                "duration_range": [duration_min, duration_max] if duration_min or duration_max else None,
                "min_rms_energy": min_rms_energy,
            }
        }
        
    except Exception as e:
        error_msg = f"Failed to search samples by attributes: {e}"
        logger.error(error_msg, exc_info=True)
        return {"error": error_msg}


@mcp.tool()
def suggest_compatible_samples(
    reference_file: str,
    count: int = 10,
    bpm_tolerance: float = 5.0,
    match_key: bool = True
) -> Dict[str, Any]:
    """Find samples compatible with a reference file based on tempo and key.
    
    Analyzes the reference file and searches for samples with similar tempo
    and optionally matching key. Useful for finding complementary samples
    for layering or arrangement.
    
    Args:
        reference_file: Path to the reference audio file
        count: Maximum number of suggestions to return (default: 10)
        bpm_tolerance: BPM range tolerance (e.g., 5.0 means ±5 BPM) (default: 5.0)
        match_key: If True, only return samples in the same key (default: True)
        
    Returns:
        Dictionary with suggested compatible samples and compatibility scores
    """
    try:
        if not Path(reference_file).is_file():
            return {"error": f"Reference file not found: {reference_file}"}
        
        # Get reference metadata
        ref_result = get_sample_metadata(reference_file)
        if "error" in ref_result:
            return ref_result
        
        ref_metadata = ref_result["metadata"]
        ref_bpm = ref_metadata.get("estimated_tempo_bpm", 120.0)
        ref_key = ref_metadata.get("estimated_key", "C")
        
        logger.info(f"Reference file: BPM={ref_bpm}, Key={ref_key}")
        
        # Search for compatible samples
        cache = _load_sample_metadata()
        
        if not cache:
            return {
                "error": "No sample metadata cache found. Use batch_analyze_samples to analyze samples first.",
                "suggestions": []
            }
        
        suggestions = []
        
        for file_path, metadata in cache.items():
            # Skip the reference file itself
            if file_path == reference_file:
                continue
            
            # Validate cache
            if not is_cache_valid(file_path, metadata.get("mtime")):
                continue
            
            sample_bpm = metadata.get("estimated_tempo_bpm", 120.0)
            sample_key = metadata.get("estimated_key", "C")
            
            # Check BPM tolerance
            bpm_diff = abs(sample_bpm - ref_bpm)
            if bpm_diff > bpm_tolerance:
                continue
            
            # Check key match if required
            if match_key and sample_key.upper() != ref_key.upper():
                continue
            
            # Calculate compatibility score (0-100)
            # Based on BPM similarity
            bpm_score = max(0, 100 - (bpm_diff / bpm_tolerance * 100))
            
            # Key match bonus
            key_score = 100 if sample_key.upper() == ref_key.upper() else 0
            
            # Overall score (weighted average)
            compatibility_score = (bpm_score * 0.7 + key_score * 0.3)
            
            suggestions.append({
                "file_path": file_path,
                "metadata": metadata,
                "compatibility_score": round(compatibility_score, 1),
                "bpm_difference": round(bpm_diff, 1),
                "key_match": sample_key.upper() == ref_key.upper(),
            })
        
        # Sort by compatibility score (descending)
        suggestions.sort(key=lambda x: x["compatibility_score"], reverse=True)
        
        # Limit results
        suggestions = suggestions[:count]
        
        logger.info(f"Found {len(suggestions)} compatible samples")
        
        return {
            "reference_file": reference_file,
            "reference_bpm": ref_bpm,
            "reference_key": ref_key,
            "suggestions": suggestions,
            "count": len(suggestions),
            "criteria": {
                "bpm_tolerance": bpm_tolerance,
                "match_key": match_key,
            }
        }
        
    except Exception as e:
        error_msg = f"Failed to suggest compatible samples: {e}"
        logger.error(error_msg, exc_info=True)
        return {"error": error_msg}
