from __future__ import annotations

import logging
import tempfile
import base64 as _b64
from pathlib import Path
from typing import Any, Dict, List, Optional

import pretty_midi as pm
import reapy
from reapy import reascript_api as RPR

from reaper_mcp.core.mcp_core import mcp
from reaper_mcp.core.util import _generate_midi_filename

logger = logging.getLogger(__name__)


@mcp.tool()
def add_midi_to_track(
    track_index: int,
    notes: List[Dict[str, Any]],
    start_time: float = 0.0,
    quantize_qn: Optional[float] = None,
) -> Dict[str, Any]:
    """Add a list of MIDI notes to a track as a new MIDI item.

    Args:
        track_index: Track index (0-based) to add MIDI to
        notes: List of dicts with keys: start (s), end (s), pitch (0-127), velocity (1-127), channel (0-15)
        start_time: Offset seconds for the item
        quantize_qn: If provided, quantize note starts/ends to this quarter-note grid
    
    Note: If you receive a 422 error, ensure numeric parameters (track_index, start_time, quantize_qn)
          are sent as numbers, not strings.
    """
    logger.info(f"add_midi_to_track called with track_index={track_index}, start_time={start_time}, "
                f"quantize_qn={quantize_qn}, notes count={len(notes) if notes else 0}")
    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]
        item_start = float(start_time)
        item_length = max((float(n["end"]) for n in notes), default=0.0)
        # Create new MIDI item in project
        item = track.add_midi_item(start=item_start, end=item_start + item_length)
        # Insert notes
        for nd in notes:
            start = float(nd.get("start", 0.0))
            end = float(nd.get("end", start + 0.25))
            channel = int(nd.get("channel", 0))
            pitch = int(nd.get("pitch", 60))
            velocity = int(nd.get("velocity", 100))
            item.add_note(pitch=pitch, start=start, end=end, velocity=velocity, channel=channel)
        logger.info(f"Successfully added {len(notes)} MIDI notes to track {track_index}")
        return {"ok": True}
    except Exception as e:
        error_msg = f"Failed to add MIDI: {e}"
        logger.error(error_msg, exc_info=True)
        return {"error": error_msg}


@mcp.tool()
def generate_midi_pattern(
    root_midi_note: int = 60,
    scale: str = "major",
    bars: int = 1,
    steps_per_bar: int = 16,
    velocity: int = 96,
) -> Dict[str, Any]:
    """Generate a simple step-sequenced MIDI pattern using pretty_midi-style output.

    Returns a list of notes dicts you can feed to add_midi_to_track.
    """
    try:
        steps = bars * steps_per_bar
        qn_per_step = 4.0 / steps_per_bar
        bpm = 120.0
        try:
            project = reapy.Project()
            bpm = project.bpm
        except Exception:
            pass
        sec_per_qn = 60.0 / bpm
        # Simple scale intervals
        scales = {
            "major": [0, 2, 4, 5, 7, 9, 11, 12],
            "minor": [0, 2, 3, 5, 7, 8, 10, 12],
            "pentatonic": [0, 3, 5, 7, 10, 12],
        }
        intervals = scales.get(scale.lower(), scales["major"])
        notes = []
        for i in range(steps):
            degree = intervals[i % len(intervals)]
            pitch = int(root_midi_note + degree)
            start_qn = i * qn_per_step
            end_qn = start_qn + qn_per_step
            start_s = start_qn * sec_per_qn
            end_s = end_qn * sec_per_qn
            notes.append({
                "start": start_s,
                "end": end_s,
                "pitch": pitch,
                "velocity": int(velocity),
                "channel": 0,
            })
        return {"notes": notes, "bpm": bpm}
    except Exception as e:
        return {"error": f"Failed to generate MIDI: {e}"}


@mcp.tool()
def generate_pretty_midi(
    root_midi_note: int = 60,
    scale: str = "major",
    bars: int = 1,
    steps_per_bar: int = 16,
    velocity: int = 96,
    program: int = 0,
) -> Dict[str, Any]:
    """Generate a MIDI file and save it locally using pretty_midi following a simple step sequence.

    Returns: { file_path: str, bpm: float, notes_count: int, duration: float }
    
    The generated MIDI file is saved in the reaper_mcp/generated_midi directory and can be
    imported to a track using import_sample_to_track().
    """
    try:
        # Determine BPM from REAPER if possible
        bpm = 120.0
        try:
            project = reapy.Project()
            bpm = project.bpm
        except Exception:
            pass
        steps = max(1, int(bars * steps_per_bar))
        qn_per_step = 4.0 / steps_per_bar
        sec_per_qn = 60.0 / bpm

        scales = {
            "major": [0, 2, 4, 5, 7, 9, 11, 12],
            "minor": [0, 2, 3, 5, 7, 8, 10, 12],
            "pentatonic": [0, 3, 5, 7, 10, 12],
        }
        intervals = scales.get(scale.lower(), scales["major"])

        midi = pm.PrettyMIDI(initial_tempo=bpm)
        inst = pm.Instrument(program=max(0, min(127, int(program))))
        for i in range(steps):
            degree = intervals[i % len(intervals)]
            pitch = int(root_midi_note + degree)
            start_s = (i * qn_per_step) * sec_per_qn
            end_s = ((i + 1) * qn_per_step) * sec_per_qn
            inst.notes.append(pm.Note(velocity=int(velocity), pitch=pitch, start=start_s, end=end_s))
        midi.instruments.append(inst)

        # Generate unique filename and save
        file_path = _generate_midi_filename(prefix=f"{scale}_{root_midi_note}")
        midi.write(str(file_path))
        
        duration = steps * qn_per_step * sec_per_qn
        logger.info(f"Generated MIDI file saved to: {file_path}")
        
        return {
            "file_path": str(file_path),
            "bpm": bpm,
            "notes_count": steps,
            "duration": duration,
        }
    except Exception as e:
        error_msg = f"Failed to generate pretty_midi: {e}"
        logger.error(error_msg, exc_info=True)
        return {"error": error_msg}


@mcp.tool()
def add_midi_file_to_track(track_index: int, midi_base64: str, insert_time: float = 0.0) -> Dict[str, Any]:
    """Import a MIDI file (base64-encoded .mid data) onto the given track at time position.
    
    Args:
        track_index: Track index (0-based) to add MIDI file to
        midi_base64: Base64-encoded MIDI file data
        insert_time: Time position in seconds to insert the MIDI
    
    Note: If you receive a 422 error, ensure numeric parameters (track_index, insert_time)
          are sent as numbers, not strings.
    """
    logger.info(f"add_midi_file_to_track called with track_index={track_index}, insert_time={insert_time}")
    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]

        # Decode to temp file
        data = _b64.b64decode(midi_base64.encode("ascii"))
        with tempfile.NamedTemporaryFile(suffix=".mid", delete=False) as tf:
            tf.write(data)
            temp_path = tf.name
        try:
            # Use REAPER's InsertMedia API to import the MIDI 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(temp_path, 0)
            
            logger.info(f"Successfully added MIDI file to track {track_index} at time {insert_time}")
            return {"ok": True}
        finally:
            try:
                Path(temp_path).unlink()
            except Exception:
                pass
    except Exception as e:
        error_msg = f"Failed to add MIDI file: {e}"
        logger.error(error_msg, exc_info=True)
        return {"error": error_msg}


@mcp.tool()
def generate_chord_progression(
    chords: List[str],
    key: str = "C",
    duration_per_chord: float = 1.0,
    voicing: str = "triad",
    octave: int = 4,
    velocity: int = 80,
) -> Dict[str, Any]:
    """Generate MIDI notes for a chord progression.
    
    Args:
        chords: List of chord symbols (e.g., ["C", "Am", "F", "G"] or ["I", "vi", "IV", "V"])
        key: Root key if using Roman numerals (default: "C")
        duration_per_chord: Duration of each chord in seconds
        voicing: "triad" (3 notes), "seventh" (4 notes), or "power" (2 notes)
        octave: Base octave for chord roots (default: 4)
        velocity: MIDI velocity (1-127)
    
    Returns:
        Dictionary with notes list suitable for add_midi_to_track
    """
    try:
        # Note names
        note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        
        # Chord quality intervals (semitones from root)
        chord_intervals = {
            "": [0, 4, 7],           # Major triad
            "m": [0, 3, 7],          # Minor triad
            "7": [0, 4, 7, 10],      # Dominant 7th
            "maj7": [0, 4, 7, 11],   # Major 7th
            "m7": [0, 3, 7, 10],     # Minor 7th
            "dim": [0, 3, 6],        # Diminished
            "aug": [0, 4, 8],        # Augmented
            "sus2": [0, 2, 7],       # Suspended 2nd
            "sus4": [0, 5, 7],       # Suspended 4th
            "power": [0, 7],         # Power chord
        }
        
        # Convert Roman numerals if needed
        roman_to_degree = {
            "I": 0, "II": 2, "III": 4, "IV": 5, "V": 7, "VI": 9, "VII": 11,
            "i": 0, "ii": 2, "iii": 4, "iv": 5, "v": 7, "vi": 9, "vii": 11,
        }
        
        # Get key root note
        key = key.replace("b", "#")
        if key not in note_names:
            return {"error": f"Invalid key: {key}"}
        key_root = note_names.index(key)
        
        notes = []
        current_time = 0.0
        
        for chord_symbol in chords:
            # Parse chord symbol
            chord_symbol = chord_symbol.strip()
            
            # Check if it's a Roman numeral
            is_roman = chord_symbol.lstrip("b#").rstrip("7maj").upper() in roman_to_degree
            
            if is_roman:
                # Roman numeral chord
                base = chord_symbol.rstrip("7maj")
                is_minor = base[0].islower()
                degree = roman_to_degree.get(base.upper(), 0)
                root_midi = (octave + 1) * 12 + ((key_root + degree) % 12)
                
                # Determine quality from Roman numeral
                if "7" in chord_symbol:
                    quality = "m7" if is_minor else "7"
                else:
                    quality = "m" if is_minor else ""
            else:
                # Named chord (e.g., "C", "Am", "F7")
                root_name = chord_symbol[0]
                if len(chord_symbol) > 1 and chord_symbol[1] in "#b":
                    root_name += chord_symbol[1]
                    quality = chord_symbol[2:] if len(chord_symbol) > 2 else ""
                else:
                    quality = chord_symbol[1:] if len(chord_symbol) > 1 else ""
                
                # Normalize root
                root_name = root_name.replace("b", "#")
                if root_name not in note_names:
                    return {"error": f"Invalid chord root: {root_name}"}
                
                root_midi = (octave + 1) * 12 + note_names.index(root_name)
            
            # Get intervals for this chord type
            intervals = chord_intervals.get(quality, chord_intervals[""])
            
            # Apply voicing filter
            if voicing == "power":
                intervals = [0, 7]
            elif voicing == "triad":
                intervals = intervals[:3]
            # seventh and others use full intervals
            
            # Generate notes for this chord
            for interval in intervals:
                notes.append({
                    "start": current_time,
                    "end": current_time + duration_per_chord,
                    "pitch": root_midi + interval,
                    "velocity": velocity,
                    "channel": 0,
                })
            
            current_time += duration_per_chord
        
        return {
            "notes": notes,
            "chord_count": len(chords),
            "duration": current_time,
        }
    except Exception as e:
        logger.error(f"Failed to generate chord progression: {e}", exc_info=True)
        return {"error": f"Failed to generate chord progression: {e}"}


@mcp.tool()
def generate_drum_pattern(
    style: str = "rock",
    bars: int = 1,
    complexity: float = 0.5,
    swing: float = 0.0,
    velocity: int = 100,
) -> Dict[str, Any]:
    """Generate a drum pattern with kick, snare, and hi-hat.
    
    Args:
        style: "rock", "jazz", "edm", "hiphop", "trap"
        bars: Number of bars (4 beats per bar)
        complexity: 0.0 to 1.0 (sparse to busy)
        swing: 0.0 to 1.0 (straight to swing timing)
        velocity: Base MIDI velocity (1-127)
    
    Returns:
        Dictionary with notes list using GM drum mapping (kick=36, snare=38, hihat=42)
    """
    try:
        import random
        
        # Get BPM from REAPER if possible
        bpm = 120.0
        try:
            project = reapy.Project()
            bpm = project.bpm
        except Exception:
            pass
        
        sec_per_beat = 60.0 / bpm
        total_beats = bars * 4
        
        # GM Drum MIDI notes
        KICK = 36
        SNARE = 38
        HIHAT_CLOSED = 42
        HIHAT_OPEN = 46
        
        notes = []
        
        # Define patterns by style
        style = style.lower()
        
        if style == "rock":
            # Rock: Kick on 1,3; Snare on 2,4; Hi-hat on 8ths
            for beat in range(total_beats):
                time = beat * sec_per_beat
                
                # Kick on beats 0, 2 (1st and 3rd)
                if beat % 4 in [0, 2]:
                    notes.append({"start": time, "end": time + 0.1, "pitch": KICK, "velocity": velocity, "channel": 9})
                
                # Snare on beats 1, 3 (2nd and 4th)
                if beat % 4 in [1, 3]:
                    notes.append({"start": time, "end": time + 0.1, "pitch": SNARE, "velocity": velocity, "channel": 9})
                
                # Hi-hat on 8th notes
                for eighth in range(2):
                    eighth_time = time + (eighth * sec_per_beat / 2)
                    if random.random() < (0.5 + complexity * 0.5):
                        notes.append({"start": eighth_time, "end": eighth_time + 0.05, "pitch": HIHAT_CLOSED, "velocity": velocity - 20, "channel": 9})
        
        elif style == "edm":
            # EDM: Four-on-the-floor kick, claps on 2,4, hi-hat on 16ths
            for beat in range(total_beats):
                time = beat * sec_per_beat
                
                # Kick on every beat
                notes.append({"start": time, "end": time + 0.1, "pitch": KICK, "velocity": velocity, "channel": 9})
                
                # Snare/clap on 2,4
                if beat % 4 in [1, 3]:
                    notes.append({"start": time, "end": time + 0.1, "pitch": SNARE, "velocity": velocity, "channel": 9})
                
                # Hi-hat on 16th notes (complexity-dependent)
                sixteenths = int(4 * complexity) + 2
                for sixteenth in range(sixteenths):
                    sixteenth_time = time + (sixteenth * sec_per_beat / 4)
                    notes.append({"start": sixteenth_time, "end": sixteenth_time + 0.03, "pitch": HIHAT_CLOSED, "velocity": velocity - 30, "channel": 9})
        
        elif style == "hiphop":
            # Hip-hop: Syncopated kick, snare on 2,4, hi-hat variations
            for beat in range(total_beats):
                time = beat * sec_per_beat
                
                # Kick pattern
                if beat % 4 == 0:
                    notes.append({"start": time, "end": time + 0.1, "pitch": KICK, "velocity": velocity, "channel": 9})
                elif beat % 4 == 2 and complexity > 0.3:
                    notes.append({"start": time + sec_per_beat * 0.5, "end": time + sec_per_beat * 0.5 + 0.1, "pitch": KICK, "velocity": velocity - 10, "channel": 9})
                
                # Snare on 2,4
                if beat % 4 in [1, 3]:
                    notes.append({"start": time, "end": time + 0.1, "pitch": SNARE, "velocity": velocity, "channel": 9})
                
                # Hi-hat pattern
                for eighth in range(2):
                    eighth_time = time + (eighth * sec_per_beat / 2)
                    vel = velocity - 20 if eighth == 0 else velocity - 35
                    notes.append({"start": eighth_time, "end": eighth_time + 0.05, "pitch": HIHAT_CLOSED, "velocity": vel, "channel": 9})
        
        elif style == "trap":
            # Trap: Sparse kick, tight snare rolls, rapid hi-hats
            for beat in range(total_beats):
                time = beat * sec_per_beat
                
                # Sparse kick pattern
                if beat % 4 == 0 or (beat % 4 == 2 and complexity > 0.5):
                    notes.append({"start": time, "end": time + 0.1, "pitch": KICK, "velocity": velocity, "channel": 9})
                
                # Snare on 2,4 with possible rolls
                if beat % 4 in [1, 3]:
                    notes.append({"start": time, "end": time + 0.1, "pitch": SNARE, "velocity": velocity, "channel": 9})
                    # Add snare rolls based on complexity
                    if complexity > 0.7 and beat % 8 == 7:
                        for roll in range(3):
                            roll_time = time + 0.15 + (roll * 0.05)
                            notes.append({"start": roll_time, "end": roll_time + 0.03, "pitch": SNARE, "velocity": velocity - 20, "channel": 9})
                
                # Rapid hi-hats
                for sixteenth in range(4):
                    sixteenth_time = time + (sixteenth * sec_per_beat / 4)
                    notes.append({"start": sixteenth_time, "end": sixteenth_time + 0.03, "pitch": HIHAT_CLOSED, "velocity": velocity - 25, "channel": 9})
        
        elif style == "jazz":
            # Jazz: Swing ride pattern, light kick, snare variations
            for beat in range(total_beats):
                time = beat * sec_per_beat
                
                # Light kick pattern
                if beat % 4 == 0 or (beat % 4 == 2 and complexity > 0.4):
                    notes.append({"start": time, "end": time + 0.1, "pitch": KICK, "velocity": velocity - 20, "channel": 9})
                
                # Snare accents
                if beat % 4 in [1, 3] and random.random() < 0.7:
                    notes.append({"start": time, "end": time + 0.1, "pitch": SNARE, "velocity": velocity - 30, "channel": 9})
                
                # Swing ride pattern on hi-hat
                for triplet in range(3):
                    triplet_time = time + (triplet * sec_per_beat / 3)
                    # Apply swing
                    if triplet == 1:
                        triplet_time += (sec_per_beat / 6) * swing
                    vel = velocity - 15 if triplet == 0 else velocity - 35
                    notes.append({"start": triplet_time, "end": triplet_time + 0.05, "pitch": HIHAT_CLOSED, "velocity": vel, "channel": 9})
        
        else:
            return {"error": f"Invalid style: {style}. Must be 'rock', 'jazz', 'edm', 'hiphop', or 'trap'"}
        
        return {
            "notes": notes,
            "style": style,
            "bars": bars,
            "duration": total_beats * sec_per_beat,
            "bpm": bpm,
        }
    except Exception as e:
        logger.error(f"Failed to generate drum pattern: {e}", exc_info=True)
        return {"error": f"Failed to generate drum pattern: {e}"}


@mcp.tool()
def humanize_midi(
    notes: List[Dict[str, Any]],
    timing_variance: float = 0.02,
    velocity_variance: int = 10,
) -> Dict[str, Any]:
    """Add subtle timing and velocity variations to MIDI notes to make them sound more natural.
    
    Args:
        notes: List of note dicts with 'start', 'end', 'pitch', 'velocity' keys
        timing_variance: Maximum timing shift in seconds (default: 0.02 = 20ms)
        velocity_variance: Maximum velocity change (default: 10)
    
    Returns:
        Dictionary with humanized notes
    """
    try:
        import random
        
        if not notes:
            return {"error": "No notes provided"}
        
        humanized = []
        for note in notes:
            if "start" not in note or "pitch" not in note:
                return {"error": "Each note must have 'start' and 'pitch' keys"}
            
            new_note = note.copy()
            
            # Add timing variation (uniform random around original time)
            timing_shift = random.uniform(-timing_variance, timing_variance)
            new_note["start"] = max(0, note["start"] + timing_shift)
            
            if "end" in note:
                # Keep note duration roughly the same
                duration = note["end"] - note["start"]
                new_note["end"] = new_note["start"] + duration
            
            # Add velocity variation
            if "velocity" in note:
                vel_shift = random.randint(-velocity_variance, velocity_variance)
                new_vel = note["velocity"] + vel_shift
                new_note["velocity"] = max(1, min(127, new_vel))
            
            humanized.append(new_note)
        
        return {
            "notes": humanized,
            "count": len(humanized),
            "timing_variance": timing_variance,
            "velocity_variance": velocity_variance,
        }
    except Exception as e:
        logger.error(f"Failed to humanize MIDI: {e}", exc_info=True)
        return {"error": f"Failed to humanize MIDI: {e}"}


@mcp.tool()
def generate_bass_line(
    chord_progression: List[str],
    key: str = "C",
    pattern: str = "root",
    octave: int = 2,
    duration_per_chord: float = 1.0,
    velocity: int = 90
) -> Dict[str, Any]:
    """Generate a bass line that follows a chord progression.
    
    Args:
        chord_progression: List of chord symbols (e.g., ["C", "Am", "F", "G"] or ["I", "vi", "IV", "V"])
        key: Musical key (e.g., "C", "Am", "F#")
        pattern: Bass pattern type: "root", "root-fifth", "walking" (default: "root")
        octave: Octave for bass notes (default: 2)
        duration_per_chord: Duration in seconds for each chord (default: 1.0)
        velocity: MIDI velocity (default: 90)
    
    Returns:
        Dictionary with note data compatible with add_midi_to_track()
    """
    try:
        from music21 import note, chord as m21_chord, roman, key as m21_key
        
        if not chord_progression:
            return {"error": "Chord progression cannot be empty"}
        
        if pattern not in ["root", "root-fifth", "walking"]:
            return {"error": "Pattern must be 'root', 'root-fifth', or 'walking'"}
        
        notes = []
        current_time = 0.0
        
        # Parse key
        try:
            music_key = m21_key.Key(key)
        except:
            return {"error": f"Invalid key: {key}"}
        
        for chord_symbol in chord_progression:
            # Parse chord (handle both chord symbols and Roman numerals)
            try:
                if chord_symbol.upper() in ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] or \
                   any(rn in chord_symbol for rn in ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii']):
                    # Roman numeral
                    rn = roman.RomanNumeral(chord_symbol, music_key)
                    chord_obj = rn
                else:
                    # Chord symbol
                    chord_obj = m21_chord.Chord(chord_symbol)
            except Exception as e:
                return {"error": f"Invalid chord: {chord_symbol} - {e}"}
            
            # Get root note
            root_note = chord_obj.root()
            root_pitch = root_note.pitch.midi
            
            # Adjust to correct octave
            while root_pitch >= (octave + 1) * 12:
                root_pitch -= 12
            while root_pitch < octave * 12:
                root_pitch += 12
            
            if pattern == "root":
                # Just play the root note for the entire duration
                notes.append({
                    "start": current_time,
                    "end": current_time + duration_per_chord,
                    "pitch": root_pitch,
                    "velocity": velocity,
                    "channel": 0
                })
            
            elif pattern == "root-fifth":
                # Alternate between root and fifth
                note_duration = duration_per_chord / 2
                
                # Root
                notes.append({
                    "start": current_time,
                    "end": current_time + note_duration,
                    "pitch": root_pitch,
                    "velocity": velocity,
                    "channel": 0
                })
                
                # Fifth (7 semitones above root)
                fifth_pitch = root_pitch + 7
                notes.append({
                    "start": current_time + note_duration,
                    "end": current_time + duration_per_chord,
                    "pitch": fifth_pitch,
                    "velocity": velocity - 10,
                    "channel": 0
                })
            
            elif pattern == "walking":
                # Walking bass: play root, third, fifth, and approach note
                note_duration = duration_per_chord / 4
                
                # Get chord tones
                third_pitch = root_pitch + (4 if chord_obj.isMinorTriad() else 4 if not chord_obj.isMajorTriad() else 4)
                if chord_obj.isMajorTriad():
                    third_pitch = root_pitch + 4  # Major third
                else:
                    third_pitch = root_pitch + 3  # Minor third
                
                fifth_pitch = root_pitch + 7
                
                # Root
                notes.append({
                    "start": current_time,
                    "end": current_time + note_duration,
                    "pitch": root_pitch,
                    "velocity": velocity,
                    "channel": 0
                })
                
                # Third
                notes.append({
                    "start": current_time + note_duration,
                    "end": current_time + note_duration * 2,
                    "pitch": third_pitch,
                    "velocity": velocity - 10,
                    "channel": 0
                })
                
                # Fifth
                notes.append({
                    "start": current_time + note_duration * 2,
                    "end": current_time + note_duration * 3,
                    "pitch": fifth_pitch,
                    "velocity": velocity - 10,
                    "channel": 0
                })
                
                # Approach note (one semitone below next root, or sixth if last chord)
                approach_pitch = root_pitch + 9  # Sixth as default
                notes.append({
                    "start": current_time + note_duration * 3,
                    "end": current_time + duration_per_chord,
                    "pitch": approach_pitch,
                    "velocity": velocity - 15,
                    "channel": 0
                })
            
            current_time += duration_per_chord
        
        return {
            "notes": notes,
            "chord_progression": chord_progression,
            "key": key,
            "pattern": pattern,
            "octave": octave,
            "duration": current_time,
        }
    except Exception as e:
        logger.error(f"Failed to generate bass line: {e}", exc_info=True)
        return {"error": f"Failed to generate bass line: {e}"}


@mcp.tool()
def generate_arpeggio(
    chord: str,
    key: str = "C",
    bars: int = 1,
    pattern: str = "up",
    note_duration: float = 0.25,
    octave: int = 4,
    velocity: int = 80
) -> Dict[str, Any]:
    """Generate an arpeggio pattern from a chord.
    
    Args:
        chord: Chord symbol (e.g., "C", "Am7", "Fmaj7") or Roman numeral (e.g., "I", "vi")
        key: Musical key for Roman numeral interpretation (default: "C")
        bars: Number of bars to generate (default: 1)
        pattern: Arpeggio pattern: "up", "down", "up-down", "random" (default: "up")
        note_duration: Duration of each note in seconds (default: 0.25)
        octave: Starting octave (default: 4)
        velocity: MIDI velocity (default: 80)
    
    Returns:
        Dictionary with note data compatible with add_midi_to_track()
    """
    try:
        from music21 import chord as m21_chord, roman, key as m21_key
        import random
        
        if pattern not in ["up", "down", "up-down", "random"]:
            return {"error": "Pattern must be 'up', 'down', 'up-down', or 'random'"}
        
        if bars <= 0 or bars > 64:
            return {"error": "Bars must be between 1 and 64"}
        
        # Parse key
        try:
            music_key = m21_key.Key(key)
        except:
            return {"error": f"Invalid key: {key}"}
        
        # Parse chord
        try:
            if chord.upper() in ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] or \
               any(rn in chord for rn in ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii']):
                # Roman numeral
                rn = roman.RomanNumeral(chord, music_key)
                chord_obj = rn
            else:
                # Chord symbol
                chord_obj = m21_chord.Chord(chord)
        except Exception as e:
            return {"error": f"Invalid chord: {chord} - {e}"}
        
        # Get chord tones
        chord_pitches = [p.midi for p in chord_obj.pitches]
        
        # Adjust to correct octave
        adjusted_pitches = []
        for pitch in chord_pitches:
            while pitch >= (octave + 2) * 12:
                pitch -= 12
            while pitch < octave * 12:
                pitch += 12
            adjusted_pitches.append(pitch)
        
        adjusted_pitches.sort()
        
        # Calculate total duration
        total_duration = bars * 4.0  # Assuming 4/4 time
        num_notes = int(total_duration / note_duration)
        
        notes = []
        current_time = 0.0
        
        for i in range(num_notes):
            # Select pitch based on pattern
            if pattern == "up":
                pitch = adjusted_pitches[i % len(adjusted_pitches)]
            elif pattern == "down":
                pitch = adjusted_pitches[-(i % len(adjusted_pitches)) - 1]
            elif pattern == "up-down":
                cycle_length = len(adjusted_pitches) * 2 - 2
                idx = i % cycle_length
                if idx < len(adjusted_pitches):
                    pitch = adjusted_pitches[idx]
                else:
                    pitch = adjusted_pitches[-(idx - len(adjusted_pitches) + 2)]
            elif pattern == "random":
                pitch = random.choice(adjusted_pitches)
            
            notes.append({
                "start": current_time,
                "end": current_time + note_duration,
                "pitch": pitch,
                "velocity": velocity,
                "channel": 0
            })
            
            current_time += note_duration
        
        return {
            "notes": notes,
            "chord": chord,
            "key": key,
            "pattern": pattern,
            "bars": bars,
            "note_duration": note_duration,
            "duration": current_time,
        }
    except Exception as e:
        logger.error(f"Failed to generate arpeggio: {e}", exc_info=True)
        return {"error": f"Failed to generate arpeggio: {e}"}


@mcp.tool()
def generate_melody_from_chords(
    chord_progression: List[str],
    key: str = "C",
    bars: int = 4,
    note_density: float = 0.5,
    octave: int = 5,
    velocity: int = 85
) -> Dict[str, Any]:
    """Generate a melodic line that fits a chord progression.
    
    Args:
        chord_progression: List of chord symbols (e.g., ["C", "Am", "F", "G"])
        key: Musical key (e.g., "C", "Am")
        bars: Total number of bars (default: 4)
        note_density: Notes per beat, 0.25-2.0 (default: 0.5)
        octave: Octave for melody (default: 5)
        velocity: MIDI velocity (default: 85)
    
    Returns:
        Dictionary with note data compatible with add_midi_to_track()
    """
    try:
        from music21 import chord as m21_chord, roman, key as m21_key, scale
        import random
        
        if not chord_progression:
            return {"error": "Chord progression cannot be empty"}
        
        if note_density < 0.25 or note_density > 2.0:
            return {"error": "Note density must be between 0.25 and 2.0"}
        
        if bars <= 0 or bars > 64:
            return {"error": "Bars must be between 1 and 64"}
        
        # Parse key
        try:
            music_key = m21_key.Key(key)
            scale_obj = music_key.getScale()
        except:
            return {"error": f"Invalid key: {key}"}
        
        # Get scale notes
        scale_pitches = [p.midi for p in scale_obj.pitches]
        # Adjust to melody octave
        adjusted_scale = []
        for pitch in scale_pitches:
            while pitch < octave * 12:
                pitch += 12
            while pitch >= (octave + 2) * 12:
                pitch -= 12
            adjusted_scale.append(pitch)
        adjusted_scale = sorted(set(adjusted_scale))
        
        # Calculate timing
        duration_per_chord = (bars * 4.0) / len(chord_progression)  # In quarter notes
        beat_duration = 1.0  # seconds per beat (can be adjusted)
        note_duration = beat_duration / note_density
        
        notes = []
        current_time = 0.0
        
        for chord_symbol in chord_progression:
            # Parse chord
            try:
                if chord_symbol.upper() in ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'] or \
                   any(rn in chord_symbol for rn in ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii']):
                    rn = roman.RomanNumeral(chord_symbol, music_key)
                    chord_obj = rn
                else:
                    chord_obj = m21_chord.Chord(chord_symbol)
            except Exception as e:
                return {"error": f"Invalid chord: {chord_symbol} - {e}"}
            
            # Get chord tones in melody octave
            chord_tones = []
            for p in chord_obj.pitches:
                pitch = p.midi
                while pitch < octave * 12:
                    pitch += 12
                while pitch >= (octave + 2) * 12:
                    pitch -= 12
                chord_tones.append(pitch)
            chord_tones = sorted(set(chord_tones))
            
            # Generate melody for this chord
            chord_duration = duration_per_chord * beat_duration
            chord_end_time = current_time + chord_duration
            
            while current_time < chord_end_time:
                # Prefer chord tones (70% chance) over scale notes
                if random.random() < 0.7 and chord_tones:
                    pitch = random.choice(chord_tones)
                else:
                    pitch = random.choice(adjusted_scale)
                
                # Vary note duration slightly
                actual_duration = note_duration * random.uniform(0.8, 1.2)
                actual_duration = min(actual_duration, chord_end_time - current_time)
                
                # Vary velocity slightly
                actual_velocity = velocity + random.randint(-10, 10)
                actual_velocity = max(40, min(127, actual_velocity))
                
                notes.append({
                    "start": current_time,
                    "end": current_time + actual_duration,
                    "pitch": pitch,
                    "velocity": actual_velocity,
                    "channel": 0
                })
                
                current_time += actual_duration
                
                # Small chance to add a rest
                if random.random() < 0.2:
                    current_time += note_duration * 0.5
        
        return {
            "notes": notes,
            "chord_progression": chord_progression,
            "key": key,
            "bars": bars,
            "note_density": note_density,
            "octave": octave,
            "duration": current_time,
        }
    except Exception as e:
        logger.error(f"Failed to generate melody: {e}", exc_info=True)
        return {"error": f"Failed to generate melody: {e}"}
