"""Reggaeton rhythm generators — pure functions returning note dicts per channel.

All patterns follow the DEMBOW — the foundational rhythm of reggaetón:

    Beat:     1     &     2     &     3     &     4     &
    Pos:    0.0   0.5   1.0   1.5   2.0   2.5   3.0   3.5
    Kick:    X     .     .     .     X     .     .     X    ← 1, 3, 4&
    Snare:   .     .     X     .     .     X     .     .    ← 2, 3&

The 3&-position snare hit is the signature dembow characteristic.
Everything locks to this grid.
"""

import random

# ---------------------------------------------------------------------------
# Channel constants — match SAMPLE_MAP in channel_skeleton.py
# ---------------------------------------------------------------------------
CH_P1 = 10   # perc1.wav
CH_K  = 11   # kick.wav
CH_S  = 12   # snare.wav
CH_R  = 13   # rim.wav
CH_P2 = 14   # perc2.wav
CH_H  = 15   # hihat.wav
CH_CL = 16   # clap.wav

# Note dict format: {"pos": float, "len": float, "key": int, "vel": int}
# pos  — in BEATS from start of bar 0 (bar 2 beat 3 → 2*4 + 2 = 10.0)
# len  — in beats (0.25 = 16th note at 4/4)
# key  — always 60 for drum samples (pitch irrelevant, sample just plays)
# vel  — 1–127 after applying velocity_mult

# ---------------------------------------------------------------------------
# DEMBOW PATTERN CONSTANTS
# ---------------------------------------------------------------------------

# Dembow kick: beats 1, 3, 4& (positions 0.0, 2.0, 3.5)
# The 4&-kick is what makes it dembow, not just a straight 1-3 pattern
KICK_DEMBOW_CLASSICO = [
    (0.0, 115),   # beat 1 — hard hit
    (2.0, 100),   # beat 3 — medium
    (3.5,  90),   # beat 4& — ghost/dembow accent
]

# Dembow perreo: adds kick on 2& for aggressive perreo style
KICK_DEMBOW_PERREO = [
    (0.0, 115),   # beat 1
    (1.5,  90),   # beat 2& — extra perreo kick
    (2.0,  95),   # beat 3
    (3.5, 100),   # beat 4&
]

# Trápico half-time: kicks on 1 and 3 only, simpler 808 feel
KICK_TRAPICO = [
    (0.0, 110),   # beat 1
    (2.0, 100),   # beat 3
]

# Dembow snare: beats 2 and 3& (positions 1.0, 2.5)
# The 3&-position is the NON-NEGOTIABLE syncopation — this is what makes it reggaetón
SNARE_DEMBOW = [
    (1.0, 105),   # beat 2 — primary snare hit
    (2.5, 100),   # beat 3& — THE signature dembow syncopation
]

# Snare fills: adds extra notes on 2, 3, 3.5 for chorus intensity
SNARE_FILLS = [
    (1.0, 105),   # beat 2
    (2.0,  80),   # beat 2& — fill
    (2.5, 100),   # beat 3& — dembow accent
    (3.0,  85),   # beat 3
    (3.5,  90),   # beat 4& — fill
]

# Hihat 16th with dembow accent pattern
# Accents align with dembow kick/snare positions for drive
HIHAT_16TH_DEMBOW = [
    (0.0, 85),    # beat 1 — accent
    (0.25, 55),
    (0.5,  60),
    (0.75, 55),
    (1.0, 80),    # beat 2 — accent (snare position)
    (1.25, 55),
    (1.5,  60),
    (1.75, 55),
    (2.0, 85),    # beat 3 — accent
    (2.25, 55),
    (2.5,  80),   # beat 3& — accent (snare position)
    (2.75, 55),
    (3.0,  70),   # beat 3 downbeat (medium)
    (3.25, 55),
    (3.5,  75),   # beat 4& — accent
    (3.75, 55),
]

# Offbeat hihat for intro/breakdown: plays on all & positions
HIHAT_OFFBEAT = [
    (0.5,  75),   # 1&
    (1.5,  70),   # 2&
    (2.5,  75),   # 3&
    (3.5,  70),   # 4&
]

# Clap layered with snare: matches snare positions exactly for punch
CLAP_DEMBOW = [
    (1.0, 115),   # beat 2 — reinforce snare
    (2.5, 110),   # beat 3& — reinforce dembow syncopation
]

# Congas following dembow: slap on beat 2, open tone on 3&
CONGAS_DEMBOW = [
    (1.0,  95),   # slap on beat 2 (matches snare)
    (2.5,  85),   # open tone on 3& (matches snare)
]

# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _apply_groove(note_data, groove_strength: float):
    """Apply groove timing and velocity variations to notes.

    note_data: either a list[dict] of notes OR a dict[int, list[dict]] (channel → notes).
    groove_strength: 0.0 = no effect, 1.0 = maximum groove feel.
    """
    if groove_strength <= 0:
        return note_data
    jitter = 5 + groove_strength * 10
    nudge = groove_strength * 0.02

    # Normalize to {channel: [notes]} format
    if isinstance(note_data, dict):
        channel_notes_map = note_data
    else:
        channel_notes_map = {"_single": note_data}

    for channel, notes in channel_notes_map.items():
        for n in notes:
            n["vel"] = max(1, min(127, int(n["vel"] + random.uniform(-jitter, jitter))))
            n["pos"] = max(0, n["pos"] + random.uniform(-nudge, nudge))

    return note_data

def _clamp_vel(vel: int) -> int:
    """Clamp velocity to valid MIDI range [1, 127]."""
    return max(1, min(127, vel))

def _apply_vel(base_vel: int, velocity_mult: float) -> int:
    """Multiply base velocity by velocity_mult and clamp."""
    return _clamp_vel(int(base_vel * velocity_mult))

def _note(pos: float, length: float, vel: int) -> dict:
    """Create a note dict with key=60."""
    return {"pos": pos, "len": length, "key": 60, "vel": vel}

def _build_bar(pattern: list[tuple[float, int]], bar_offset: float) -> list[dict]:
    """Build one bar of notes from a pattern list.

    Args:
        pattern: List of (beat_position, base_velocity) tuples
        bar_offset: Bar offset in beats (bar_n * 4.0)
    """
    notes = []
    for pos, base_vel in pattern:
        notes.append(_note(bar_offset + pos, 0.25, base_vel))
    return notes


# ---------------------------------------------------------------------------
# Kick generators
# ---------------------------------------------------------------------------

def kick_main_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Dembow kick — beats 1, 3, 4& (positions 0.0, 2.0, 3.5).

    This is the CLASSIC dembow kick. The 4&-position is what separates
    reggaetón from generic Latin music.
    Returns {CH_K: [notes...]}.
    """
    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for pos, base_vel in KICK_DEMBOW_CLASSICO:
            notes.append(_note(o + pos, 0.25, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_K: notes}, groove_strength)


def kick_sparse_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Trápico kick — beats 1 and 3 only (positions 0.0, 2.0).

    Half-time feel for trap/reggaetón fusion. Simpler than dembow.
    Returns {CH_K: [notes...]}.
    """
    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for pos, base_vel in KICK_TRAPICO:
            notes.append(_note(o + pos, 0.25, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_K: notes}, groove_strength)


def kick_outro_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Outro kick — dembow pattern with softer velocity.

    Delegates to kick_main_notes with 0.75 velocity baseline.
    Returns {CH_K: [notes...]}.
    """
    return kick_main_notes(bars, velocity_mult=velocity_mult * 0.75, density=density, groove_strength=groove_strength)


# ---------------------------------------------------------------------------
# Snare generators
# ---------------------------------------------------------------------------

def snare_verse_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Dembow snare — beats 2 and 3& (positions 1.0, 2.5).

    THE signature dembow characteristic. The 3&-position syncopation
    is what makes reggaetón groove the way it does.
    Returns {CH_S: [notes...]}.
    """
    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for pos, base_vel in SNARE_DEMBOW:
            notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_S: notes}, groove_strength)


def snare_fill_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Chorus snare fills — dembow base plus extras on 2, 3, 3&.

    Adds extra notes for build-up sections while keeping the 3& accent.
    Returns {CH_S: [notes...]}.
    """
    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for pos, base_vel in SNARE_FILLS:
            notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_S: notes}, groove_strength)


def snare_outro_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Outro snare — dembow with softer velocity.

    Delegates to snare_verse_notes with 0.7 velocity baseline.
    Returns {CH_S: [notes...]}.
    """
    return snare_verse_notes(bars, velocity_mult=velocity_mult * 0.7, density=density, groove_strength=groove_strength)


# ---------------------------------------------------------------------------
# Hihat generators
# ---------------------------------------------------------------------------

def hihat_16th_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """16th-note hihat with dembow accent pattern.

    Accents align with kick/snare positions (beats 1, 2, 3, 3&) to drive the groove.
    density=1.0 → all 16ths; density=0.5 → every other 16th.
    Returns {CH_H: [notes...]}.
    """
    notes: list[dict] = []
    step = max(1, round(1.0 / density)) if density > 0 else 1
    for b in range(bars):
        o = b * 4.0
        for idx, (pos, base_vel) in enumerate(HIHAT_16TH_DEMBOW):
            if idx % step == 0:
                notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_H: notes}, groove_strength)


def hihat_8th_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Offbeat hihat for intro/breakdown.

    Plays on all & positions (0.5, 1.5, 2.5, 3.5) — creates breathing room.
    Returns {CH_H: [notes...]}.
    """
    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for pos, base_vel in HIHAT_OFFBEAT:
            notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_H: notes}, groove_strength)


# ---------------------------------------------------------------------------
# Clap generator
# ---------------------------------------------------------------------------

def clap_24_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Dembow clap — beats 2 and 3& (positions 1.0, 2.5).

    Layered with snare to reinforce the dembow characteristic.
    Matches SNARE_DEMBOW positions exactly.
    Returns {CH_CL: [notes...]}.
    """
    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for pos, base_vel in CLAP_DEMBOW:
            notes.append(_note(o + pos, 0.15, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_CL: notes}, groove_strength)


def generate_clap_pattern(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Generate clap pattern using CLAP_DEMBOW.

    Snare on beats 2, 3.5 — standard reggaeton clap.
    Returns {CH_CL: [notes...]}.
    """
    return clap_24_notes(bars, velocity_mult, density, groove_strength)


# ---------------------------------------------------------------------------
# Percussion generators
# ---------------------------------------------------------------------------

def perc_combo_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Conga pattern following dembow — slap on beat 2, open on 3&.

    perc2 (CH_P2): conga slap and open tone
    perc1 (CH_P1): reserved for additional perc (not used in base pattern)
    Returns {CH_P2: [...], CH_P1: [...]}.
    """
    p2_notes: list[dict] = []
    p1_notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for pos, base_vel in CONGAS_DEMBOW:
            p2_notes.append(_note(o + pos, 0.1, _apply_vel(base_vel, velocity_mult)))
    return _apply_groove({CH_P1: p1_notes, CH_P2: p2_notes}, groove_strength)


def rim_build_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
) -> dict[int, list[dict]]:
    """Rim roll that builds intensity across a 4-bar cycle.

    Bar N%4=0: sparse (positions 0, 2, 8, 14 in 16ths)
    Bar N%4=1: filling in (positions 0, 2, 4, 8, 10, 14)
    Bar N%4=2: every other 16th (positions 0, 2, 4, 6, 8, 10, 12, 14)
    Bar N%4=3: full roll (all 16 positions)
    Velocity ramps: 50 → 65 → 80 → 100 across the cycle.
    Returns {CH_R: [notes...]}.
    """
    _PATTERNS = [
        [0, 2, 8, 14],
        [0, 2, 4, 8, 10, 14],
        [0, 2, 4, 6, 8, 10, 12, 14],
        list(range(16)),
    ]
    _BASE_VELS = [50, 65, 80, 100]

    notes: list[dict] = []
    for b in range(bars):
        cycle = b % 4
        o = b * 4.0
        base_vel = _BASE_VELS[cycle]
        vel = _apply_vel(base_vel, velocity_mult)
        for idx in _PATTERNS[cycle]:
            notes.append(_note(o + idx * 0.25, 0.1, vel))
    return _apply_groove({CH_R: notes}, groove_strength)


# ---------------------------------------------------------------------------
# Registry & dispatcher
# ---------------------------------------------------------------------------

GENERATORS: dict[str, callable] = {
    "kick_main_notes":   kick_main_notes,
    "kick_sparse_notes": kick_sparse_notes,
    "kick_outro_notes":  kick_outro_notes,
    "snare_verse_notes": snare_verse_notes,
    "snare_fill_notes":  snare_fill_notes,
    "snare_outro_notes": snare_outro_notes,
    "hihat_16th_notes":  hihat_16th_notes,
    "hihat_8th_notes":   hihat_8th_notes,
    "clap_24_notes":     clap_24_notes,
    "perc_combo_notes":  perc_combo_notes,
    "rim_build_notes":   rim_build_notes,
}


# ---------------------------------------------------------------------------
# Pattern-bank-aware generators (new — use extracted real-track data)
# ---------------------------------------------------------------------------

def kick_pattern_bank_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
    bank: str = "dembow_classico",
) -> dict[int, list[dict]]:
    """Generate kick notes from a named pattern bank.

    Args:
        bars: Number of bars
        velocity_mult: Velocity multiplier per section energy
        density: 1.0 = all notes in pattern, 0.5 = every other, etc.
        groove_strength: Groove amount (0.0 = none)
        bank: Pattern bank name — "dembow_classico", "perreo", "trapico", "dense"
    """
    from src.composer.patterns import get_pattern_bank, PatternEntry

    pattern = get_pattern_bank(bank)
    step = max(1, round(1.0 / density)) if density > 0 else 1

    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for idx, entry in enumerate(pattern):
            if idx % step == 0:
                notes.append(_note(
                    o + entry.pos,
                    entry.len,
                    _apply_vel(entry.vel, velocity_mult),
                ))
    return _apply_groove({CH_K: notes}, groove_strength)


def snare_pattern_bank_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
    bank: str = "dembow_classico",
) -> dict[int, list[dict]]:
    """Generate snare notes from a named pattern bank.

    Uses SNARE_DEMBOW_CLASSICO positions by default (beats 2 and 3&).
    """
    from src.composer.patterns import SNARE_DEMBOW_CLASSICO

    pattern = SNARE_DEMBOW_CLASSICO
    step = max(1, round(1.0 / density)) if density > 0 else 1

    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for idx, entry in enumerate(pattern):
            if idx % step == 0:
                notes.append(_note(
                    o + entry.pos,
                    entry.len,
                    _apply_vel(entry.vel, velocity_mult),
                ))
    return _apply_groove({CH_S: notes}, groove_strength)


def hihat_pattern_bank_notes(
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
    bank: str = "dembow_classico",
) -> dict[int, list[dict]]:
    """Generate hihat notes from a named pattern bank.

    bank="dembow_classico" → 16th-note grid with dembow accents
    bank="offbeat" → offbeat positions only (for breakdowns)
    """
    from src.composer.patterns import HIHAT_DEMBOW_CLASSICO, HIHAT_OFFBEAT

    pattern = HIHAT_OFFBEAT if bank == "offbeat" else HIHAT_DEMBOW_CLASSICO
    step = max(1, round(1.0 / density)) if density > 0 else 1

    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        for idx, entry in enumerate(pattern):
            if idx % step == 0:
                notes.append(_note(
                    o + entry.pos,
                    entry.len,
                    _apply_vel(entry.vel, velocity_mult),
                ))
    return _apply_groove({CH_H: notes}, groove_strength)


# Extended registry including pattern-bank generators
PATTERN_GENERATORS: dict[str, callable] = {
    "kick_pattern_bank_notes":  kick_pattern_bank_notes,
    "snare_pattern_bank_notes": snare_pattern_bank_notes,
    "hihat_pattern_bank_notes": hihat_pattern_bank_notes,
    # Fallback to legacy generators
    "kick_main_notes":   kick_main_notes,
    "kick_sparse_notes": kick_sparse_notes,
    "snare_verse_notes": snare_verse_notes,
    "snare_fill_notes":  snare_fill_notes,
    "hihat_16th_notes":  hihat_16th_notes,
    "hihat_8th_notes":   hihat_8th_notes,
    "clap_24_notes":     clap_24_notes,
    "perc_combo_notes":  perc_combo_notes,
    "rim_build_notes":   rim_build_notes,
}


def get_notes(
    generator_name: str,
    bars: int,
    velocity_mult: float = 1.0,
    density: float = 1.0,
    groove_strength: float = 0.0,
    bank: str = "dembow_classico",
) -> dict[int, list[dict]]:
    """Dispatch to the named generator.

    Pattern-bank-aware generators accept `bank` parameter.
    Legacy generators ignore the bank parameter.
    """
    # Try pattern generators first, fall back to legacy
    if generator_name in PATTERN_GENERATORS:
        gen = PATTERN_GENERATORS[generator_name]
    elif generator_name in GENERATORS:
        gen = GENERATORS[generator_name]
    else:
        raise KeyError(f"Unknown generator: {generator_name}")

    # Pattern-bank generators accept bank parameter
    import inspect
    sig = inspect.signature(gen)
    if "bank" in sig.parameters:
        return gen(bars, velocity_mult, density, groove_strength, bank=bank)
    return gen(bars, velocity_mult, density, groove_strength)