#!/usr/bin/env python
"""REAPER .rpp reggaeton generator — based on proven Ableton arrangement.

Uses REAL drumloop samples from the Ableton library (not scored random ones),
and a PROVEN harmonic bass pattern from a working Ableton project.

Drumloop arrangement: seco → filtrado → vacío → seco (repeating per section)
808 Bass: i - iv - i - V pattern (A1 → D2 → A1 → E2) from Ableton project
No vocals — instrumental only.

Usage:
    python scripts/compose.py --output output/song.rpp
    python scripts/compose.py --bpm 99 --key Am --output output/song.rpp
"""
from __future__ import annotations

import argparse
import random
import sys
from pathlib import Path

_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(_ROOT))

from src.core.schema import (
    SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote, CCEvent,
    PluginDef, SectionDef,
)
from src.selector import SampleSelector
from src.reaper_builder import RPPBuilder, PLUGIN_REGISTRY, PLUGIN_PRESETS
from src.composer.chords import ChordEngine
from src.composer.melody_engine import build_motif, build_call_response
from src.calibrator import Calibrator

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
NOTE_TO_MIDI = {n: i for i, n in enumerate(NOTE_NAMES)}

# Ableton drumloop paths (proven to sound good)
ABLETON_DRUMLOOP_DIR = Path(
    r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts"
    r"\libreria\reggaeton\drumloops"
)

# Drumloop arrangement per section:
# Each section gets a drumloop variant: "seco", "filtrado", "empty", "seco"
# This cycles through the sections
DRUMLOOP_ASSIGNMENTS = {
    "intro":      "filtrado",   # filtered intro
    "verse":      "seco",       # dry verse
    "pre-chorus": "filtrado",   # building with filter
    "chorus":     "seco",       # full energy dry
    "verse2":     "seco",       # dry verse 2
    "chorus2":    "seco",       # full energy dry
    "bridge":     "filtrado",   # filtered bridge
    "final":      "seco",       # full energy
    "outro":      "filtrado",   # filtered outro
}

# Drumloop files for each variant
DRUMLOOP_FILES = {
    "seco": [
        "90bpm reggaeton antiguo drumloop.wav",
        "94bpm reggaeton antiguo 2 drumloop.wav",
        "100bpm_gata-only_drumloop.wav",
    ],
    "filtrado": [
        "100bpm filtrado drumloop.wav",
        "100bpm contigo filtrado drumloop.wav",
    ],
}

# 808 Bass pattern — reggaeton i-iv-i-V, one note every 2 bars on beat 1.0.
# Sparse pattern: 808 tail (1.5 beat duration) fills the space between hits.
# Am: A1(33) → D2(38) → A1(33) → E2(40)
# Duration: 1.5 beats, velocity varies by section
BASS_PATTERN_8BARS = [
    {"pitch": 33, "start_time": 0.0,  "duration": 1.5, "velocity": 80},   # Bar 1-2: i
    {"pitch": 38, "start_time": 8.0,  "duration": 1.5, "velocity": 80},   # Bar 3-4: iv
    {"pitch": 33, "start_time": 16.0, "duration": 1.5, "velocity": 80},   # Bar 5-6: i
    {"pitch": 40, "start_time": 24.0, "duration": 1.5, "velocity": 80},   # Bar 7-8: V
]

# Section structure from Ableton project
SECTIONS = [
    ("intro",       4, 0.3, False),
    ("verse",       8, 0.5, True),
    ("pre-chorus",  4, 0.7, False),
    ("chorus",      8, 1.0, True),
    ("verse2",      8, 0.5, True),
    ("chorus2",     8, 1.0, True),
    ("bridge",      4, 0.4, False),
    ("final",       8, 1.0, True),
    ("outro",       4, 0.3, False),
]

# Clap positions: beats 2.0 and 3.5 in each bar (reggaeton dembow)
CLAP_POSITIONS = [2.0, 3.5]

# Chord progression i-VI-III-VII (reggaeton standard)
CHORD_PROGRESSION = [
    (0, "minor"),   # i
    (8, "major"),   # VI
    (3, "major"),   # III
    (10, "major"),  # VII
]

# FX chains per track role
FX_CHAINS = {
    "drumloop": ["Decapitator", "Radiator"],
    "bass":     ["Serum_2", "Decapitator", "Gullfoss_Master"],
    "chords":   ["Omnisphere", "PhaseMistress", "EchoBoy"],
    "lead":     ["Serum_2", "Tremolator"],
    "clap":     ["Decapitator"],
    "pad":      ["Omnisphere", "ValhallaDelay"],
    "perc":     ["Decapitator"],
}

SEND_LEVELS = {
    "bass":           (0.05, 0.02),
    "chords":         (0.15, 0.08),
    "lead":           (0.10, 0.05),
    "clap":           (0.05, 0.02),
    "pad":            (0.25, 0.15),
    "perc":           (0.05, 0.02),
    "transition_fx":  (0.08, 0.05),
}

VOLUME_LEVELS = {
    "drumloop": 0.85,
    "bass":     0.72,
    "chords":   0.70,
    "lead":     0.75,
    "clap":     0.80,
    "pad":      0.65,
    "perc":     0.78,
}

MASTER_VOLUME = 0.85

# ---------------------------------------------------------------------------
# FX Transitions — glue sections with risers, impacts, sweeps
# ---------------------------------------------------------------------------

FX_ROLE = "fx"

# Map: boundary section index → (type, start_offset, length, fade_in, fade_out)
# or list of tuples for boundaries with multiple FX (e.g. riser + impact).
# Position = offsets[boundary_idx] * 4 + start_offset
# Types: riser (before climax with long fade_in), impact (on downbeat with fade_out),
#        sweep/transition (brief bridge, both fades)
FX_TRANSITIONS: dict[int, tuple | list[tuple]] = {
    2: ("sweep", -2, 2, 0.3, 0.0),      # verse→pre-chorus (beat 48 → pos 46)
    3: [                                     # pre-chorus→chorus (beat 64)
        ("riser",  -4, 4, 1.5, 0.0),       # pos 60 — builds into chorus
        ("impact",  0, 2, 0.0, 0.3),       # pos 64 — hits on chorus downbeat
    ],
    4: ("transition", -2, 2, 0.2, 0.2),  # chorus→verse2 (beat 96 → pos 94)
    5: ("riser", -4, 4, 1.0, 0.0),        # verse2→chorus2 (beat 128 → pos 124)
    6: ("sweep", -2, 2, 0.2, 0.2),        # chorus2→bridge (beat 160 → pos 158)
    7: ("riser", -4, 4, 1.0, 0.0),        # bridge→final (beat 176 → pos 172)
    8: ("sweep", -2, 2, 0.3, 0.5),        # final→outro (beat 208 → pos 206)
}

# Section energy — which tracks play in each section type
TRACK_ACTIVITY: dict[str, dict[str, bool]] = {
    "intro":      {"drumloop": True,  "perc": False, "bass": False, "chords": False, "lead": False, "clap": False, "pad": True},
    "verse":      {"drumloop": True,  "perc": False, "bass": True,  "chords": True,  "lead": False, "clap": False, "pad": False},
    "pre-chorus": {"drumloop": True,  "perc": False, "bass": True,  "chords": True,  "lead": True,  "clap": True,  "pad": False},
    "chorus":     {"drumloop": True,  "perc": True,  "bass": True,  "chords": True,  "lead": True,  "clap": True,  "pad": True},
    "verse2":     {"drumloop": True,  "perc": False, "bass": True,  "chords": True,  "lead": False, "clap": False, "pad": False},
    "chorus2":    {"drumloop": True,  "perc": True,  "bass": True,  "chords": True,  "lead": True,  "clap": True,  "pad": True},
    "bridge":     {"drumloop": True,  "perc": False, "bass": False, "chords": False, "lead": True,  "clap": False, "pad": True},
    "final":      {"drumloop": True,  "perc": True,  "bass": True,  "chords": True,  "lead": True,  "clap": True,  "pad": True},
    "outro":      {"drumloop": True,  "perc": False, "bass": False, "chords": False, "lead": False, "clap": False, "pad": True},
}

# (velocity_mult, vol_mult) per section type
SECTION_MULTIPLIERS: dict[str, tuple[float, float]] = {
    "intro":      (0.6, 0.70),
    "verse":      (0.7, 0.85),
    "pre-chorus": (0.85, 0.95),
    "chorus":     (1.0, 1.00),
    "verse2":     (0.7, 0.85),
    "chorus2":    (1.0, 1.00),
    "bridge":     (0.6, 0.75),
    "final":      (1.0, 1.00),
    "outro":      (0.4, 0.60),
}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def key_to_midi_root(key_str: str, octave: int = 2) -> int:
    root = key_str.rstrip("m")
    return NOTE_TO_MIDI[root] + (octave + 1) * 12


def parse_key(key_str: str) -> tuple[str, bool]:
    if key_str.endswith("m"):
        return key_str[:-1], True
    return key_str, False


def build_chord(root_midi: int, quality: str) -> list[int]:
    if quality == "minor":
        return [root_midi, root_midi + 3, root_midi + 7]
    return [root_midi, root_midi + 4, root_midi + 7]


def get_pentatonic(root: str, is_minor: bool, octave: int) -> list[int]:
    root_midi = key_to_midi_root(root, octave)
    intervals = [0, 3, 5, 7, 10] if is_minor else [0, 2, 4, 7, 9]
    return [root_midi + i for i in intervals]


def make_plugin(registry_key: str, index: int, role: str = "") -> PluginDef:
    if registry_key in PLUGIN_REGISTRY:
        display, path, uid = PLUGIN_REGISTRY[registry_key]
        preset = PLUGIN_PRESETS.get((registry_key, role)) or PLUGIN_PRESETS.get((registry_key, ""))
        return PluginDef(name=registry_key, path=path, index=index, preset_data=preset, role=role)
    return PluginDef(name=registry_key, path=registry_key, index=index, role=role)


def _section_active(section_name: str, role: str, activity: dict) -> bool:
    """Return whether a track role is active in the given section type."""
    return activity.get(section_name, {}).get(role, False)


def build_section_structure():
    sections = []
    for n, b, e, _ in SECTIONS:
        vm, vol = SECTION_MULTIPLIERS.get(n, (1.0, 1.0))
        sections.append(SectionDef(name=n, bars=b, energy=e, velocity_mult=vm, vol_mult=vol))
    offsets = []
    off = 0.0
    for sec in sections:
        offsets.append(off)
        off += sec.bars
    return sections, offsets


# ---------------------------------------------------------------------------
# Sidechain kick cache
# ---------------------------------------------------------------------------

_kick_cache: dict[str, list[float]] = {}
_LOOP_DURATIONS: dict[str, float] = {}
_KICK_CONFIDENCE_THRESHOLD = 0.6

_CC11_DIP = 50
_CC11_HOLD = 0.02
_CC11_RELEASE = 0.18


def _get_kick_cache(drumloop_paths: list[str], bpm: float) -> dict[str, list[float]]:
    """Analyze drumloops, return {path: [kick_time_beats]}.

    Uses module-level _kick_cache for deduplication across calls.
    Kicks are filtered by confidence >= _KICK_CONFIDENCE_THRESHOLD.
    Time is converted from seconds to beats using bpm.
    """
    from src.composer.drum_analyzer import DrumLoopAnalyzer

    for path in drumloop_paths:
        if path in _kick_cache:
            continue
        try:
            analyzer = DrumLoopAnalyzer(path)
            analysis = analyzer.analyze()
            kicks = [
                t for t in analysis.transients
                if t.type == "kick" and t.confidence >= _KICK_CONFIDENCE_THRESHOLD
            ]
            beat_dur = 60.0 / bpm
            _kick_cache[path] = [t.time / beat_dur for t in kicks]
            _LOOP_DURATIONS[path] = analysis.duration / beat_dur
        except Exception:
            _kick_cache[path] = []

    return _kick_cache


# ---------------------------------------------------------------------------
# Track Builders
# ---------------------------------------------------------------------------

def pick_drumloop(variant: str, index: int = 0) -> str | None:
    """Pick a drumloop file for the given variant (seco/filtrado)."""
    files = DRUMLOOP_FILES.get(variant, [])
    if not files:
        return None
    name = files[index % len(files)]
    path = ABLETON_DRUMLOOP_DIR / name
    return str(path) if path.exists() else None


def build_drumloop_track(sections, offsets, seed: int = 0) -> TrackDef:
    """Drumloop track: different sample per section (seco/filtrado/empty cycle)."""
    rng = random.Random(seed)
    clips = []
    seco_idx = 0
    filtrado_idx = 0

    for section, sec_off in zip(sections, offsets):
        # Skip sections where drumloop is not active
        if not _section_active(section.name, "drumloop", TRACK_ACTIVITY):
            continue

        # Determine variant
        variant = DRUMLOOP_ASSIGNMENTS.get(section.name, "seco")

        if variant == "empty":
            continue  # no drumloop in this section

        if variant == "seco":
            path = pick_drumloop("seco", seco_idx)
            seco_idx += 1
        else:
            path = pick_drumloop("filtrado", filtrado_idx)
            filtrado_idx += 1

        if path:
            clips.append(ClipDef(
                position=sec_off * 4.0,
                length=section.bars * 4.0,
                name=f"{section.name.capitalize()} Drumloop",
                audio_path=path,
                loop=True,
                vol_mult=section.vol_mult,
            ))

    plugins = [make_plugin(fx, i, role="drumloop") for i, fx in enumerate(FX_CHAINS.get("drumloop", []))]
    return TrackDef(
        name="Drumloop",
        volume=VOLUME_LEVELS["drumloop"],
        pan=0.0,
        clips=clips,
        plugins=plugins,
    )


def build_perc_track(sections, offsets, seed: int = 0) -> TrackDef:
    """Percussion track: perc loops from Ableton library per section."""
    ableton_perc_dir = Path(
        r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts"
        r"\libreria\reggaeton\drumloops"
    )
    # Use specific perc files from Ableton project
    perc_files = [
        "91bpm bellako percloop.wav",
        "91bpm bellako percloop.wav",  # repeat for variety
    ]
    # Also check SentimientoLatino perc
    sentimiento_perc = Path(
        r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts"
        r"\libreria\reggaeton\SentimientoLatino2025\02\23 Drum Loops"
    )

    clips = []
    for i, (section, sec_off) in enumerate(zip(sections, offsets)):
        # Use centralized activity matrix instead of ad-hoc name check
        if not _section_active(section.name, "perc", TRACK_ACTIVITY):
            continue

        perc_name = perc_files[i % len(perc_files)]
        perc_path = ableton_perc_dir / perc_name

        if perc_path.exists():
            clips.append(ClipDef(
                position=sec_off * 4.0,
                length=section.bars * 4.0,
                name=f"{section.name.capitalize()} Perc",
                audio_path=str(perc_path),
                loop=True,
                vol_mult=section.vol_mult,
            ))

    plugins = [make_plugin(fx, i, role="perc") for i, fx in enumerate(FX_CHAINS.get("perc", []))]
    return TrackDef(
        name="Perc",
        volume=VOLUME_LEVELS["perc"],
        pan=0.12,
        clips=clips,
        plugins=plugins,
    )


def build_bass_track(sections, offsets, key_root: str, key_minor: bool,
                     kick_cache: dict[str, list[float]] | None = None) -> TrackDef:
    """808 bass using PROVEN harmonic pattern from Ableton project.
    
    When kick_cache is provided, generates CC11 ducking events on kick hits.
    """
    root_midi = key_to_midi_root(key_root, 1)  # Octave 1 for 808

    # Transpose the Ableton pattern to match the project key
    # Ableton pattern is in Am (root=33=A1), transpose to project key
    transpose = root_midi - 33  # 33 is A1 from Ableton pattern
    kick_cache = kick_cache or {}

    clips = []
    for section, sec_off in zip(sections, offsets):
        if not _section_active(section.name, "bass", TRACK_ACTIVITY):
            continue

        velocity = int(80 * section.velocity_mult)

        notes = []
        bars = section.bars

        # Repeat the 8-bar pattern to fill the section
        pattern_notes = BASS_PATTERN_8BARS
        for repeat_start in range(0, bars, 8):
            bars_this_repeat = min(8, bars - repeat_start)
            for pn in pattern_notes:
                # Only include notes within this repeat's bar range
                bar_of_note = pn["start_time"] / 4.0
                if bar_of_note < bars_this_repeat:
                    notes.append(MidiNote(
                        pitch=pn["pitch"] + transpose,
                        start=repeat_start * 4.0 + pn["start_time"],
                        duration=pn["duration"],
                        velocity=velocity,
                    ))

        # Generate CC11 ducking events from kick cache
        cc_events: list[CCEvent] = []
        clip_start = sec_off * 4.0
        clip_end = clip_start + section.bars * 4.0

        # Collect all kick positions in this clip's time range
        clip_kicks: list[float] = []
        for drumloop_path, kicks in kick_cache.items():
            for kick_beat in kicks:
                if clip_start <= kick_beat < clip_end:
                    clip_kicks.append((kick_beat - clip_start))  # relative to clip start

        clip_kicks.sort()

        for rel_kick in clip_kicks:
            cc_events.append(CCEvent(controller=11, time=rel_kick, value=_CC11_DIP))
            cc_events.append(CCEvent(controller=11, time=rel_kick + _CC11_HOLD, value=_CC11_DIP))
            cc_events.append(CCEvent(controller=11, time=rel_kick + _CC11_RELEASE, value=127))

        if notes:
            clips.append(ClipDef(
                position=sec_off * 4.0,
                length=section.bars * 4.0,
                name=f"{section.name.capitalize()} 808",
                midi_notes=notes,
                midi_cc=cc_events,
                vol_mult=section.vol_mult,
            ))

    plugins = [make_plugin(fx, i, role="bass") for i, fx in enumerate(FX_CHAINS.get("bass", []))]
    return TrackDef(
        name="808 Bass",
        volume=VOLUME_LEVELS["bass"],
        pan=0.0,
        clips=clips,
        plugins=plugins,
    )


def build_chords_track(
    sections, offsets, key_root: str, key_minor: bool,
    emotion: str = "romantic", inversion: str = "root",
) -> TrackDef:
    """Chords: delegate to ChordEngine for progression + voice leading."""
    key = key_root + "m" if key_minor else key_root
    engine = ChordEngine(key, seed=42)

    clips = []
    for section, sec_off in zip(sections, offsets):
        if section.name in ("intro", "break", "outro"):
            continue  # no chords in sparse sections

        vm = section.energy
        voicings = engine.progression(
            section.bars, emotion=emotion,
            beats_per_chord=4, inversion=inversion,
        )

        notes = []
        for bar in range(section.bars):
            chord_idx = bar % len(voicings)
            voicing = voicings[chord_idx]
            for pitch in voicing:
                notes.append(MidiNote(
                    pitch=pitch,
                    start=bar * 4.0,
                    duration=4.0,
                    velocity=int(75 * vm),
                ))
        if notes:
            clips.append(ClipDef(
                position=sec_off * 4.0,
                length=section.bars * 4.0,
                name=f"{section.name.capitalize()} Chords",
                midi_notes=notes,
            ))

    plugins = [make_plugin(fx, i, role="chords") for i, fx in enumerate(FX_CHAINS.get("chords", []))]
    return TrackDef(
        name="Chords",
        volume=VOLUME_LEVELS["chords"],
        pan=0.0,
        clips=clips,
        plugins=plugins,
    )


def build_lead_track(sections, offsets, key_root: str, key_minor: bool, seed: int = 0) -> TrackDef:
    """Lead melody: hook-based call-response using melody_engine.

    Replaces random pentatonic generation with deterministic motif engine
    producing arch-contour hooks, chord-tone emphasis, and call-response phrasing.
    """
    clips = []

    for section, sec_off in zip(sections, offsets):
        # Lead only in sections where the lead role is active
        if not _section_active(section.name, "lead", TRACK_ACTIVITY):
            continue

        # Build a hook motif for this section (4 bars), then expand to section length
        motif = build_motif(key_root, key_minor, "hook", bars=min(4, section.bars), seed=seed)
        notes = build_call_response(motif, bars=section.bars, key_root=key_root,
                                    key_minor=key_minor, seed=seed + 1)

        # Scale velocities to section energy
        for note in notes:
            note.velocity = int(note.velocity * section.velocity_mult)

        if notes:
            clips.append(ClipDef(
                position=sec_off * 4.0,
                length=section.bars * 4.0,
                name=f"{section.name.capitalize()} Lead",
                midi_notes=notes,
                vol_mult=section.vol_mult,
            ))

    plugins = [make_plugin(fx, i, role="lead") for i, fx in enumerate(FX_CHAINS.get("lead", []))]
    return TrackDef(
        name="Lead",
        volume=VOLUME_LEVELS["lead"],
        pan=0.0,
        clips=clips,
        plugins=plugins,
    )


def build_clap_track(selector: SampleSelector, sections, offsets) -> TrackDef:
    """Clap: snare samples on beats 2.0 and 3.5, ONLY in chorus/verse sections."""
    snare_results = selector.select(role="snare", limit=3)
    clap_path = snare_results[0].sample["original_path"] if snare_results else None

    clips = []
    if clap_path:
        for section, sec_off in zip(sections, offsets):
            if not _section_active(section.name, "clap", TRACK_ACTIVITY):
                continue
            for bar in range(section.bars):
                for cb in CLAP_POSITIONS:
                    clips.append(ClipDef(
                        position=sec_off * 4.0 + bar * 4.0 + cb,
                        length=0.5,
                        name=f"{section.name.capitalize()} Clap",
                        audio_path=clap_path,
                        vol_mult=section.vol_mult,
                    ))

    plugins = [make_plugin(fx, i, role="clap") for i, fx in enumerate(FX_CHAINS.get("clap", []))]
    return TrackDef(
        name="Clap",
        volume=VOLUME_LEVELS["clap"],
        pan=0.0,
        clips=clips,
        plugins=plugins,
    )


def build_fx_track(
    sections, offsets, selector: SampleSelector, seed: int = 0,
) -> TrackDef:
    """Build a dedicated transition FX track with audio clips at section boundaries.

    Uses SampleSelector.select_one(role="fx") to pick FX samples from the library.
    FX_TRANSITIONS maps boundary section indices to (type, offset, length, fade_in, fade_out).
    Boundary 3 (pre-chorus→chorus) has two entries: riser BEFORE the boundary and impact ON it.

    Clip positions are computed as: offsets[boundary_idx] * 4 + start_offset.
    Risers have fade_in > 0 (build-up); impacts have fade_out > 0 (hit); sweeps have both.
    """
    clips = []
    fx_idx = 0

    for boundary_idx, entries in sorted(FX_TRANSITIONS.items()):
        # Normalise: single tuple → list of tuples
        items = [entries] if isinstance(entries, tuple) else entries

        for fx_type, start_offset, length, fade_in, fade_out in items:
            boundary_beat = offsets[boundary_idx] * 4.0
            position = boundary_beat + start_offset

            # Select one FX sample per clip for variety
            sample = selector.select_one(role=FX_ROLE, seed=seed + fx_idx)
            fx_idx += 1

            audio_path = sample.get("original_path") if sample else None

            clips.append(ClipDef(
                position=position,
                length=float(length),
                name=f"{fx_type.capitalize()} FX",
                audio_path=audio_path,
                fade_in=float(fade_in),
                fade_out=float(fade_out),
            ))

    return TrackDef(
        name="Transition FX",
        volume=0.72,
        pan=0.0,
        clips=clips,
        send_level={0: 0.08, 1: 0.05},
    )


def build_pad_track(sections, offsets, key_root: str, key_minor: bool) -> TrackDef:
    """Pad: arpeggiated chord, cycling through chord tones on eighth notes.
    
    Each section gets an ascending arpeggio cycling through chord notes
    at octave 3 (low, avoids clashing with chords at octave 4).
    Replaces the old static sustained pad with rhythmic movement.
    """
    root_midi = key_to_midi_root(key_root, 3)
    quality = "minor" if key_minor else "major"
    chord = build_chord(root_midi, quality)

    clips = []
    for section, sec_off in zip(sections, offsets):
        if not _section_active(section.name, "pad", TRACK_ACTIVITY):
            continue

        velocity = int(55 * section.velocity_mult)
        total_beats = section.bars * 4.0
        notes = []
        beat = 0.0
        while beat < total_beats:
            pitch = chord[int(beat * 2) % len(chord)]  # ascend through chord tones
            notes.append(MidiNote(
                pitch=pitch,
                start=beat,
                duration=0.5,
                velocity=velocity,
            ))
            beat += 0.5  # eighth note step

        clips.append(ClipDef(
            position=sec_off * 4.0,
            length=section.bars * 4.0,
            name=f"{section.name.capitalize()} Pad",
            midi_notes=notes,
            vol_mult=section.vol_mult,
        ))

    plugins = [make_plugin(fx, i, role="pad") for i, fx in enumerate(FX_CHAINS.get("pad", []))]
    return TrackDef(
        name="Pad",
        volume=0.55,  # lower volume to prevent masking chords
        pan=0.0,
        clips=clips,
        plugins=plugins,
    )


def create_return_tracks() -> list[TrackDef]:
    return [
        TrackDef(
            name="Reverb",
            volume=0.7,
            pan=0.0,
            clips=[],
            plugins=[make_plugin("FabFilter_Pro-R_2", 0)],
        ),
        TrackDef(
            name="Delay",
            volume=0.7,
            pan=0.0,
            clips=[],
            plugins=[make_plugin("ValhallaDelay", 0)],
        ),
    ]


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Compose a REAPER .rpp reggaeton — based on proven Ableton arrangement."
    )
    parser.add_argument("--bpm", type=float, default=99, help="BPM (default: 99)")
    parser.add_argument("--key", default="Am", help="Key (default: Am)")
    parser.add_argument("--output", default="output/song.rpp", help="Output path")
    parser.add_argument("--seed", type=int, default=None, help="Random seed")
    parser.add_argument(
        "--emotion", default="romantic",
        choices=["romantic", "dark", "club", "classic"],
        help="Emotion mode for chord progression (default: romantic)",
    )
    parser.add_argument(
        "--inversion", default="root",
        choices=["root", "first", "second"],
        help="Chord inversion preference (default: root)",
    )
    parser.add_argument(
        "--no-calibrate", action="store_true",
        help="Skip post-processing mix calibration",
    )
    args = parser.parse_args()

    if args.seed is not None:
        random.seed(args.seed)

    output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    bpm = args.bpm
    if bpm <= 0:
        raise ValueError(f"bpm must be > 0, got {bpm}")
    key = args.key
    key_root, key_minor = parse_key(key)

    print(f"Project: {bpm} BPM, Key: {key}")

    # Load sample selector for clap/snare
    index_path = _ROOT / "data" / "sample_index.json"
    selector = SampleSelector(str(index_path))
    selector._load()

    # Build sections
    sections, offsets = build_section_structure()
    total_beats = sum(s.bars for s in sections) * 4.0
    print(f"Sections: {len(sections)}, Total: {int(total_beats/4)} bars ({total_beats} beats)")

    # Build kick cache from unique drumloop paths
    drumloop_track = build_drumloop_track(sections, offsets, seed=args.seed or 0)
    drumloop_paths = sorted({c.audio_path for c in drumloop_track.clips if c.audio_path})
    if drumloop_paths:
        _get_kick_cache(drumloop_paths, bpm)
        for path, kicks in _kick_cache.items():
            print(f"  Kick cache: {len(kicks)} kicks in {Path(path).name}")

        # Project relative kicks to absolute timeline positions per clip
        _abs_kicks: dict[str, list[float]] = {}
        for clip in drumloop_track.clips:
            path = clip.audio_path
            if not path or path not in _kick_cache:
                continue
            rel_kicks = _kick_cache[path]
            if not rel_kicks:
                continue
            loop_beats = _LOOP_DURATIONS.get(path, 8.0)
            abs_positions: list[float] = []
            loop_n = 0
            while loop_n * loop_beats < clip.length:
                offset = clip.position + loop_n * loop_beats
                for k in rel_kicks:
                    abs_pos = offset + k
                    if abs_pos < clip.position + clip.length:
                        abs_positions.append(abs_pos)
                loop_n += 1
            if path in _abs_kicks:
                _abs_kicks[path].extend(abs_positions)
            else:
                _abs_kicks[path] = abs_positions
        _kick_cache.clear()
        _kick_cache.update(_abs_kicks)

    # Build tracks
    tracks = [
        build_drumloop_track(sections, offsets, seed=args.seed or 0),
        build_perc_track(sections, offsets, seed=args.seed or 0),
        build_bass_track(sections, offsets, key_root, key_minor, kick_cache=_kick_cache),
        build_chords_track(sections, offsets, key_root, key_minor,
                           emotion=args.emotion, inversion=args.inversion),
        build_lead_track(sections, offsets, key_root, key_minor, seed=args.seed or 42),
        build_clap_track(selector, sections, offsets),
        build_fx_track(sections, offsets, selector, seed=args.seed or 0),
        build_pad_track(sections, offsets, key_root, key_minor),
    ]

    return_tracks = create_return_tracks()
    all_tracks = tracks + return_tracks

    # Wire sends
    reverb_idx = len(tracks)
    delay_idx = len(tracks) + 1
    for track in all_tracks:
        if track.name in ("Reverb", "Delay"):
            continue
        role = track.name.lower().replace(" ", "_")
        sends = SEND_LEVELS.get(role, (0.0, 0.0))
        track.send_level = {reverb_idx: sends[0], delay_idx: sends[1]}

    # Assemble
    meta = SongMeta(
        bpm=bpm, key=key, title="Reggaeton Instrumental",
        calibrate=not args.no_calibrate,
    )
    song = SongDefinition(
        meta=meta,
        tracks=all_tracks,
        sections=sections,
        master_plugins=["Pro-Q_3", "Pro-C_2", "Pro-L_2"],
    )

    # Post-processing mix calibration (unless --no-calibrate)
    if meta.calibrate:
        Calibrator.apply(song)

    errors = song.validate()
    if errors:
        print("WARNING: validation errors:", file=sys.stderr)
        for e in errors[:10]:
            print(f"  - {e}", file=sys.stderr)

    builder = RPPBuilder(song, seed=args.seed)
    builder.write(str(output_path))
    print(f"\nWritten: {output_path.resolve()}")


# Backward compat stubs (used by tests)
EFFECT_ALIASES: dict = {}

def build_section_tracks(*args, **kwargs):
    return [], []

def build_fx_chain(*args, **kwargs):
    return []

def build_sampler_plugin(*args, **kwargs):
    return None

def build_melody_track(sections, offsets, key_root, key_minor, seed=0):
    return build_lead_track(sections, offsets, key_root, key_minor, seed=seed)
