"""Melodic pattern generators for reggaetón production.

All patterns follow reggaetón theory:
- Bass follows TRESILLO 3-3-2 grouping
- Chords follow i-VI-III-VII progression (Am-F-C-G)
- Lead syncopation matches dembow (beats 2 and 3&)
- Pad follows chord progression with sustained notes

Designed to feed MelodicTrack notes in SongDefinition.
"""

import random

# ---------------------------------------------------------------------------
# Scale definitions
# ---------------------------------------------------------------------------

SCALES = {
    "minor":    [0, 2, 3, 5, 7, 8, 10],   # natural minor
    "major":    [0, 2, 4, 5, 7, 9, 11],
    "phrygian": [0, 1, 3, 5, 7, 8, 10],
    "dorian":   [0, 2, 3, 5, 7, 9, 10],
}

ROOT_SEMITONE = {
    "C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3,
    "E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, "G#": 8,
    "Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11,
}


# ---------------------------------------------------------------------------
# Chord progressions — i-VI-III-VII is THE reggaetón progression
# ---------------------------------------------------------------------------

# Semitone intervals from root for each chord in the progression
# classic_minor: Am - F - C - G (i - VI - III - VII in natural minor)
_CHORD_INTERVALLS = {
    # i (root position): 0, 3, 7 (root, minor 3rd, perfect 5th)
    # VI: F = F(5) + 8 semitones from A (0). F(5) + 8 = A(9) + 8 = 17 → octave = 5. Intervals: (8, 0, 4)
    # III: C = C(0) + 4 semitones from A. Intervals: (4, 7, 11)
    # VII: G = G(7) + 10 semitones from A (raised 7th in harmonic minor). Intervals: (10, 1, 5)
    "classic_minor": {
        "intervals": [(0, 3, 7), (8, 0, 4), (4, 7, 11), (10, 1, 5)],
        "root_offsets": [0, 8, 4, 10],   # root semitone offsets for each chord
    },
    # tension: Am - F - C - G but with different voicing feel
    "tension": {
        "intervals": [(0, 3, 7), (8, 0, 4), (4, 7, 11), (10, 1, 5)],
        "root_offsets": [0, 8, 4, 10],
    },
    # romantic: Am - G - F - E (i - VII - VI - V in natural minor)
    "romantic": {
        "intervals": [(0, 3, 7), (10, 1, 5), (8, 0, 4), (4, 9, 0)],
        "root_offsets": [0, 10, 8, 4],
    },
}


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

def _parse_key(key_str: str) -> tuple[int, str]:
    """Parse a key like 'Am', 'C#m', 'Dm', 'C' into (root_semitone, scale_name)."""
    if key_str.endswith("m") and key_str != "m":
        root_str = key_str[:-1]
        scale_name = "minor"
    else:
        root_str = key_str
        scale_name = "major"
    root = ROOT_SEMITONE.get(root_str)
    if root is None:
        raise ValueError(f"Unknown root: {root_str}")
    return root, scale_name


def _get_scale_notes(root: int, scale: str, octave: int) -> list[int]:
    """Return MIDI note numbers for all degrees of the scale in given octave."""
    intervals = SCALES.get(scale, SCALES["major"])
    return [root + octave * 12 + interval for interval in intervals]


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


def _apply_humanize(notes: list[dict], humanize: float) -> list[dict]:
    """Apply humanization (velocity jitter + position nudge) to note list."""
    if humanize <= 0:
        return notes
    jitter = humanize * 5
    nudge = humanize * 0.03
    for n in notes:
        n["vel"] = _clamp_vel(int(n["vel"] + random.uniform(-jitter, jitter)))
        n["pos"] = max(0, n["pos"] + random.uniform(-nudge, nudge))
    return notes


# ---------------------------------------------------------------------------
# Bass: tresillo 3-3-2 grouping — NOW section-aware
# ---------------------------------------------------------------------------

def bass_tresillo(
    key: str,
    bars: int,
    octave: int = 3,
    velocity_mult: float = 1.0,
    humanize: float = 0.0,
    section_type: str = "verse",
) -> list[dict]:
    """Reggaetón tresillo bass pattern — section-aware version.

    The 3-3-2 grouping is the BASS DNA of reggaetón:
    - 3 notes in first group (positions 0.0, 0.75, 1.5)
    - 3 notes in second group (positions 2.25, 3.0, 3.75)
    - 2 notes would complete the next bar...

    But for 6/8 feel within 4/4, we do 6 notes per bar:
    Positions: [0.0, 0.75, 1.5, 2.25, 3.0, 3.75]
              ↑     ↑     ↑     ↑     ↑     ↑
              3     3     2     (groups)

    Root note on downbeats (0.0, 1.5, 3.0)
    Fifth on upbeats (0.75, 2.25, 3.75)
    Velocity: Root=110, Fifth=90

    Default octave=3 gives root in MIDI range 45-52 (A3-E4), good for 808.

    Section-aware behavior:
    - chorus/drop: full velocity, root on beat 1
    - verse/bridge: slightly softer (0.85x), stripped
    - break/gap: minimal (0.3x), few notes
    - intro: building (0.5x then ramp)
    """
    root, scale = _parse_key(key)
    root_note = root + (octave - 1) * 12   # octave 3 → MIDI 45 for A
    fifth_note = root_note + 7             # perfect fifth above root

    # Section-aware velocity and density adjustments
    section = section_type.lower()
    if section in ("chorus", "drop"):
        vel_root = 115
        vel_fifth = 95
        density_mult = 1.0
    elif section in ("verse", "bridge"):
        vel_root = 95
        vel_fifth = 75
        density_mult = 1.0
    elif section in ("break", "gap"):
        vel_root = 40
        vel_fifth = 30
        density_mult = 0.3   # sparse
    elif section == "intro":
        vel_root = 70
        vel_fifth = 55
        density_mult = 0.6   # building up
    else:
        vel_root = 100
        vel_fifth = 80
        density_mult = 0.9

    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        # 6 positions per bar for the 3-3-2 tresillo feel
        positions = [0.0, 0.75, 1.5, 2.25, 3.0, 3.75]
        for idx, pos in enumerate(positions):
            if idx % 2 == 0:   # downbeats: root (positions 0, 2, 4)
                key_note = root_note
                vel = vel_root
            else:              # upbeats: fifth (positions 1, 3, 5)
                key_note = fifth_note
                vel = vel_fifth

            # Apply density — skip some notes when density_mult < 1
            if random.random() > density_mult:
                continue

            vel = _clamp_vel(int(vel * velocity_mult))
            notes.append({"pos": o + pos, "len": 0.5, "key": key_note, "vel": vel})

    return _apply_humanize(notes, humanize)


# ---------------------------------------------------------------------------
# Chords: block chords following i-VI-III-VII — section-aware
# ---------------------------------------------------------------------------

def chords_block(
    key: str,
    bars: int,
    octave: int = 4,
    beats_per_chord: int = 4,
    velocity_mult: float = 1.0,
    humanize: float = 0.0,
    section_type: str = "verse",
) -> list[dict]:
    """Block chords following the i-VI-III-VII progression — section-aware.

    Default progression: Am - F - C - G (classic_minor)
    Chord positions: every beats_per_chord beats
    Each chord: root + third + fifth (3 notes stacked at same position)

    Section-aware behavior:
    - chorus/drop: full velocity, tight voicings
    - verse/bridge: softer (0.7x), wider voicings
    - break/gap: near-silence
    - intro: soft, building
    """
    root, scale = _parse_key(key)

    # Use classic_minor progression for minor keys
    progression_key = "classic_minor"
    if scale != "minor":
        progression_key = "major"

    prog_data = _CHORD_INTERVALLS.get(progression_key, _CHORD_INTERVALLS["classic_minor"])
    chord_intervals = prog_data["intervals"]

    # Section-aware velocity adjustments
    section = section_type.lower()
    if section in ("chorus", "drop"):
        base_vel = 90
        voicing_spread = 0.0   # tight
    elif section in ("verse", "bridge"):
        base_vel = 65
        voicing_spread = 0.3   # wider, more open
    elif section in ("break", "gap"):
        base_vel = 30
        voicing_spread = 0.5
    elif section == "intro":
        base_vel = 55
        voicing_spread = 0.2
    else:
        base_vel = 75
        voicing_spread = 0.1

    notes: list[dict] = []
    total_beats = bars * 4.0
    pos = 0.0

    while pos < total_beats:
        chord_idx = int(pos / beats_per_chord) % len(chord_intervals)
        intervals = chord_intervals[chord_idx]

        # Add root, 3rd, 5th for this chord
        for interval in intervals:
            midi_note = root + octave * 12 + interval
            # Spread 3rd and 5th slightly for wider voicings
            if interval == intervals[1]:   # third
                midi_note += round(voicing_spread * 12)
            elif interval == intervals[2]:  # fifth
                midi_note += round(voicing_spread * 6)

            vel = _clamp_vel(int(base_vel * velocity_mult))
            notes.append({
                "pos": pos,
                "len": beats_per_chord - 0.25,
                "key": midi_note,
                "vel": vel,
            })

        pos += beats_per_chord

    return _apply_humanize(notes, humanize)


# ---------------------------------------------------------------------------
# Lead: hook with dembow syncopation — now section-aware
# ---------------------------------------------------------------------------

# Scale degrees for reggaetón hook melody (pentatonic-based for catchiness)
_HOOK_DEGREES = [0, 2, 4, 2, 3, 1, 0, 2, 4, 5, 4, 2, 0]   # 13-note motif


def lead_hook(
    key: str,
    bars: int,
    octave: int = 5,
    density: float = 0.6,
    velocity_mult: float = 1.0,
    humanize: float = 0.0,
    section_type: str = "verse",
) -> list[dict]:
    """Lead hook with dembow syncopation — section-aware version.

    The hook emphasizes the DEMBOW syncopation — notes on beats 2, 3&, 4
    (positions 4.0, 11.0, 12.0 from the real-track analysis).

    Scale: Pentatonic or minor for reggaetón feel
    Density: 0.6 means some slots are empty for groove

    Section-aware behavior:
    - chorus/drop: higher density, longer notes on dembow positions
    - verse/bridge: sparser, softer
    - break/gap: near-silence (sparse, very soft)
    - intro: building up from sparse
    """
    root, scale = _parse_key(key)
    intervals = SCALES.get(scale, SCALES["major"])

    # Build scale notes for octave 5 and 6
    scale_notes_oct5 = _get_scale_notes(root, scale, octave)    # 7 notes
    scale_notes_oct6 = _get_scale_notes(root, scale, octave + 1)

    # Section-aware adjustments
    section = section_type.lower()
    if section in ("chorus", "drop"):
        base_density = 0.8
        base_vel = 110
        length_factor = 1.0
    elif section in ("verse", "bridge"):
        base_density = 0.5
        base_vel = 85
        length_factor = 0.8
    elif section in ("break", "gap"):
        base_density = 0.15
        base_vel = 40
        length_factor = 0.5
    elif section == "intro":
        base_density = 0.4
        base_vel = 70
        length_factor = 0.7
    else:
        base_density = 0.6
        base_vel = 95
        length_factor = 0.9

    # Combine function param density with section density
    effective_density = density * base_density

    notes: list[dict] = []

    # Dembow accent positions from real track analysis: 4, 8, 11, 12
    # These are beats 2, 3, 3&, 4 in 16th-note positions
    dembow_positions = [4.0, 8.0, 11.0, 12.0]

    pos = 0.0
    degree_idx = 0
    step = max(1, round(1.0 / effective_density)) if effective_density > 0 else 1

    slot = 0
    while pos < bars * 4.0:
        slot_half = int(pos * 2)   # 0.5-beat slots

        if slot_half % step == 0:
            # Check if this position is a dembow accent position
            is_dembow = any(abs(pos - dp) < 0.01 for dp in dembow_positions)

            # Alternate between octave 5 and 6 for contour
            use_oct6 = (degree_idx // 2) % 3 == 0
            scale_notes = scale_notes_oct6 if use_oct6 else scale_notes_oct5
            midi_note = scale_notes[_HOOK_DEGREES[degree_idx % len(_HOOK_DEGREES)] % 7]

            # Duration: longer on dembow positions for emphasis
            length = 1.0 * length_factor if is_dembow else 0.5 * length_factor
            vel = base_vel if is_dembow else int(base_vel * 0.75)
            vel = _clamp_vel(int(vel * velocity_mult))

            notes.append({"pos": pos, "len": length, "key": midi_note, "vel": vel})

            degree_idx = (degree_idx + 1) % len(_HOOK_DEGREES)

            # Advance by length
            pos += 0.5   # fixed 8th-note step
        else:
            pos += 0.5

    return _apply_humanize(notes, humanize)


# ---------------------------------------------------------------------------
# Pad: sustain following chord progression
# ---------------------------------------------------------------------------

def pad_sustain(
    key: str,
    bars: int,
    octave: int = 4,
    velocity_mult: float = 1.0,
    humanize: float = 0.0,
    section_type: str = "verse",
) -> list[dict]:
    """Sustained pad notes following the i-VI-III-VII chord progression.

    One note per bar (root of each chord)
    Duration: 3.5 beats (leaves gap for next bar)
    Velocity: 65-75 (soft, atmospheric)
    Follows chord progression from chords_block.

    Section-aware behavior:
    - chorus/drop: full velocity (75)
    - verse/bridge: softer (55)
    - break/gap: near-silence (25)
    - intro: building (45)
    """
    root, scale = _parse_key(key)

    # Section-aware velocity
    section = section_type.lower()
    if section in ("chorus", "drop"):
        base_vel = 75
    elif section in ("verse", "bridge"):
        base_vel = 55
    elif section in ("break", "gap"):
        base_vel = 25
    elif section == "intro":
        base_vel = 45
    else:
        base_vel = 65

    # Use classic_minor progression
    prog_data = _CHORD_INTERVALLS.get("classic_minor", _CHORD_INTERVALLS["classic_minor"])
    root_offsets = prog_data["root_offsets"]   # [0, 8, 4, 10]

    notes: list[dict] = []
    for b in range(bars):
        o = b * 4.0
        chord_idx = b % 4
        root_interval = root_offsets[chord_idx]
        midi_note = root + octave * 12 + root_interval

        vel = _clamp_vel(int(base_vel * velocity_mult))
        notes.append({
            "pos": o,
            "len": 3.5,    # sustained for most of the bar
            "key": midi_note,
            "vel": vel,
        })

    return _apply_humanize(notes, humanize)