from __future__ import annotations

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

from reaper_mcp.core.mcp_core import mcp

logger = logging.getLogger(__name__)

# Musical constants
NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
SCALE_INTERVALS = {
    "major": [0, 2, 4, 5, 7, 9, 11],
    "minor": [0, 2, 3, 5, 7, 8, 10],
    "harmonic_minor": [0, 2, 3, 5, 7, 8, 11],
    "melodic_minor": [0, 2, 3, 5, 7, 9, 11],
    "dorian": [0, 2, 3, 5, 7, 9, 10],
    "phrygian": [0, 1, 3, 5, 7, 8, 10],
    "lydian": [0, 2, 4, 6, 7, 9, 11],
    "mixolydian": [0, 2, 4, 5, 7, 9, 10],
    "locrian": [0, 1, 3, 5, 6, 8, 10],
    "pentatonic_major": [0, 2, 4, 7, 9],
    "pentatonic_minor": [0, 3, 5, 7, 10],
    "blues": [0, 3, 5, 6, 7, 10],
    "chromatic": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
}

# Common chord progressions by mood
CHORD_PROGRESSIONS = {
    "happy": [
        ["I", "V", "vi", "IV"],  # Pop progression
        ["I", "IV", "V", "I"],   # Classic progression
        ["I", "vi", "IV", "V"],  # 50s progression
    ],
    "sad": [
        ["vi", "IV", "I", "V"],  # Sad pop
        ["i", "VI", "III", "VII"],  # Minor progression
        ["i", "iv", "v", "i"],   # Classic minor
    ],
    "dark": [
        ["i", "VII", "VI", "VII"],
        ["i", "v", "i", "iv"],
        ["i", "VI", "III", "VII"],
    ],
    "uplifting": [
        ["I", "V", "vi", "IV"],
        ["IV", "I", "V", "vi"],
        ["I", "IV", "vi", "V"],
    ],
    "energetic": [
        ["I", "IV", "I", "V"],
        ["I", "V", "IV", "V"],
        ["vi", "IV", "I", "V"],
    ],
}


@mcp.tool()
def get_scale_notes(key: str, scale_type: str = "major", octave: int = 4) -> Dict[str, Any]:
    """Get MIDI note numbers for a scale starting from a given key and octave.
    
    Args:
        key: Root note (e.g., "C", "F#", "Bb")
        scale_type: Type of scale (major, minor, dorian, phrygian, lydian, mixolydian, 
                    locrian, harmonic_minor, melodic_minor, pentatonic_major, 
                    pentatonic_minor, blues, chromatic)
        octave: Starting octave (default: 4, where C4 = middle C = MIDI 60)
    
    Returns:
        Dictionary with scale notes as MIDI numbers and note names
    """
    try:
        # Normalize key name
        key = key.replace("b", "#")  # Convert flats to sharps for consistency
        if key.endswith("##"):
            # Handle double sharps
            base_idx = NOTES.index(key[0])
            key = NOTES[(base_idx + 2) % 12]
        
        if key not in NOTES:
            return {"error": f"Invalid key: {key}. Must be one of {NOTES}"}
        
        scale_type_lower = scale_type.lower().replace(" ", "_")
        if scale_type_lower not in SCALE_INTERVALS:
            return {"error": f"Invalid scale type: {scale_type}. Available: {list(SCALE_INTERVALS.keys())}"}
        
        root_note = NOTES.index(key)
        base_midi = (octave + 1) * 12 + root_note  # MIDI note number for root
        
        intervals = SCALE_INTERVALS[scale_type_lower]
        midi_notes = [base_midi + interval for interval in intervals]
        note_names = [f"{NOTES[(root_note + interval) % 12]}{octave + (root_note + interval) // 12}" 
                      for interval in intervals]
        
        return {
            "scale": f"{key} {scale_type}",
            "midi_notes": midi_notes,
            "note_names": note_names,
            "octave": octave,
        }
    except Exception as e:
        logger.error(f"Failed to get scale notes: {e}", exc_info=True)
        return {"error": f"Failed to get scale notes: {e}"}


@mcp.tool()
def transpose_notes(notes: List[Dict[str, Any]], semitones: int) -> Dict[str, Any]:
    """Transpose a list of MIDI notes by a given number of semitones.
    
    Args:
        notes: List of note dicts with at least 'pitch' key (MIDI note number)
        semitones: Number of semitones to transpose (positive = up, negative = down)
    
    Returns:
        Dictionary with transposed notes
    """
    try:
        if not notes:
            return {"error": "No notes provided"}
        
        transposed = []
        for note in notes:
            if "pitch" not in note:
                return {"error": "Each note must have a 'pitch' key"}
            
            new_note = note.copy()
            new_pitch = int(note["pitch"]) + int(semitones)
            
            # Clamp to valid MIDI range (0-127)
            if new_pitch < 0 or new_pitch > 127:
                logger.warning(f"Transposed pitch {new_pitch} out of range, clamping")
                new_pitch = max(0, min(127, new_pitch))
            
            new_note["pitch"] = new_pitch
            transposed.append(new_note)
        
        return {
            "notes": transposed,
            "semitones": semitones,
            "count": len(transposed),
        }
    except Exception as e:
        logger.error(f"Failed to transpose notes: {e}", exc_info=True)
        return {"error": f"Failed to transpose notes: {e}"}


@mcp.tool()
def find_relative_key(key: str) -> Dict[str, Any]:
    """Find the relative major/minor key.
    
    Args:
        key: Key name with optional mode (e.g., "C major", "A minor", or just "C")
    
    Returns:
        Dictionary with relative key information
    """
    try:
        # Parse key and determine if it's major or minor
        key_parts = key.strip().split()
        root = key_parts[0]
        mode = key_parts[1].lower() if len(key_parts) > 1 else "major"
        
        # Normalize key name
        root = root.replace("b", "#")
        if root not in NOTES:
            return {"error": f"Invalid key: {root}. Must be one of {NOTES}"}
        
        root_idx = NOTES.index(root)
        
        if mode.startswith("maj"):
            # Major -> relative minor (down 3 semitones)
            relative_idx = (root_idx - 3) % 12
            relative_mode = "minor"
        elif mode.startswith("min"):
            # Minor -> relative major (up 3 semitones)
            relative_idx = (root_idx + 3) % 12
            relative_mode = "major"
        else:
            return {"error": f"Mode must be 'major' or 'minor', got: {mode}"}
        
        relative_key = NOTES[relative_idx]
        
        return {
            "original_key": f"{root} {mode}",
            "relative_key": f"{relative_key} {relative_mode}",
            "root_note": relative_key,
            "mode": relative_mode,
        }
    except Exception as e:
        logger.error(f"Failed to find relative key: {e}", exc_info=True)
        return {"error": f"Failed to find relative key: {e}"}


@mcp.tool()
def suggest_chord_progression(key: str, mood: str = "happy", length: int = 4) -> Dict[str, Any]:
    """Suggest a chord progression based on mood and key.
    
    Args:
        key: Root key for the progression (e.g., "C", "F#", "Bb")
        mood: Mood/style (happy, sad, dark, uplifting, energetic)
        length: Number of chords (default: 4)
    
    Returns:
        Dictionary with suggested chord progression in Roman numerals and actual chords
    """
    try:
        # Normalize key
        key = key.replace("b", "#")
        if key not in NOTES:
            return {"error": f"Invalid key: {key}. Must be one of {NOTES}"}
        
        mood_lower = mood.lower()
        if mood_lower not in CHORD_PROGRESSIONS:
            return {"error": f"Invalid mood: {mood}. Available: {list(CHORD_PROGRESSIONS.keys())}"}
        
        # Get progressions for this mood
        progressions = CHORD_PROGRESSIONS[mood_lower]
        
        # Select progression (use first one, or cycle if length is different)
        base_progression = progressions[0]
        
        # Extend or trim to requested length
        if length > len(base_progression):
            # Repeat the progression
            progression = (base_progression * ((length // len(base_progression)) + 1))[:length]
        else:
            progression = base_progression[:length]
        
        # Convert Roman numerals to actual chord names
        root_idx = NOTES.index(key)
        
        # Major scale for major key, minor for minor progressions
        is_minor = any(numeral.islower() for numeral in progression)
        scale_intervals = SCALE_INTERVALS["minor" if is_minor else "major"]
        
        chord_names = []
        for numeral in progression:
            # Parse Roman numeral
            is_lowercase = numeral[0].islower()
            degree = numeral.upper().replace("I", "1").replace("V", "5").replace("X", "10")
            
            # Convert to scale degree (I=0, II=1, etc.)
            roman_map = {"I": 0, "II": 1, "III": 2, "IV": 3, "V": 4, "VI": 5, "VII": 6}
            degree_idx = roman_map.get(numeral.upper().rstrip("7"), 0)
            
            # Get the root note for this chord
            if degree_idx < len(scale_intervals):
                chord_root_offset = scale_intervals[degree_idx]
                chord_root = NOTES[(root_idx + chord_root_offset) % 12]
                
                # Add quality (major/minor)
                quality = "m" if is_lowercase else ""
                chord_names.append(f"{chord_root}{quality}")
            else:
                chord_names.append(numeral)
        
        return {
            "key": key,
            "mood": mood,
            "progression_roman": progression,
            "progression_chords": chord_names,
            "length": len(progression),
        }
    except Exception as e:
        logger.error(f"Failed to suggest chord progression: {e}", exc_info=True)
        return {"error": f"Failed to suggest chord progression: {e}"}


@mcp.tool()
def quantize_notes_to_scale(notes: List[Dict[str, Any]], key: str, scale_type: str = "major") -> Dict[str, Any]:
    """Quantize/snap MIDI notes to the nearest note in a given scale.
    
    Args:
        notes: List of note dicts with at least 'pitch' key
        key: Root key of the scale
        scale_type: Type of scale to quantize to
    
    Returns:
        Dictionary with quantized notes
    """
    try:
        if not notes:
            return {"error": "No notes provided"}
        
        # Get scale notes across multiple octaves
        scale_notes = []
        for oct in range(0, 11):  # Cover all MIDI octaves
            scale_data = get_scale_notes.fn(key, scale_type, oct)
            if "error" in scale_data:
                return scale_data
            scale_notes.extend(scale_data["midi_notes"])
        
        # Quantize each note
        quantized = []
        for note in notes:
            if "pitch" not in note:
                return {"error": "Each note must have a 'pitch' key"}
            
            original_pitch = int(note["pitch"])
            
            # Find nearest scale note
            nearest = min(scale_notes, key=lambda x: abs(x - original_pitch))
            
            new_note = note.copy()
            new_note["pitch"] = nearest
            new_note["original_pitch"] = original_pitch
            quantized.append(new_note)
        
        return {
            "notes": quantized,
            "scale": f"{key} {scale_type}",
            "count": len(quantized),
        }
    except Exception as e:
        logger.error(f"Failed to quantize notes: {e}", exc_info=True)
        return {"error": f"Failed to quantize notes: {e}"}
