"""Hook-based reggaeton melody engine — deterministic, chord-aware motif generation.

Replaces random pentatonic lead lines with structured repeating motifs
using call-and-response phrasing and arch-contour hooks.

Design: pure functions, no I/O, no global state.
RNG: random.Random(seed) per-call for full determinism.
"""

from __future__ import annotations

import random
from src.core.schema import MidiNote

# ---------------------------------------------------------------------------
# Musical constants
# ---------------------------------------------------------------------------

_NOTE_TO_MIDI = {
    "C": 0, "C#": 1, "D": 2, "D#": 3, "E": 4, "F": 5,
    "F#": 6, "G": 7, "G#": 8, "A": 9, "A#": 10, "B": 11,
}

# i-VI-III-VII progression (reggaeton standard) — duplicated from compose.py
_CHORD_PROGRESSION: list[tuple[int, str]] = [
    (0, "minor"),   # i
    (8, "major"),   # VI
    (3, "major"),   # III
    (10, "major"),  # VII
]

_CHORD_BARS = 2  # each chord lasts 2 bars

# Dembow stab grid (beat offsets within each bar)
_DEMBOW_POSITIONS: list[float] = [1.0, 2.5, 3.0, 3.5]


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

def _midi_for_name(note_name: str, octave: int = 4) -> int:
    """Convert note name (e.g. "A") to MIDI pitch at given octave."""
    return (octave + 1) * 12 + _NOTE_TO_MIDI[note_name]


def _get_pentatonic(key_root: str, key_minor: bool, octave: int) -> list[int]:
    """Return pentatonic scale MIDI notes for the given key and octave."""
    root_midi = _midi_for_name(key_root, octave)
    intervals = [0, 3, 5, 7, 10] if key_minor else [0, 2, 4, 7, 9]
    return [root_midi + i for i in intervals]


def _get_diatonic(key_root: str, key_minor: bool, octave: int) -> list[int]:
    """Return full diatonic scale MIDI notes (7 notes, not pentatonic)."""
    root_midi = _midi_for_name(key_root, octave)
    intervals = [0, 2, 3, 5, 7, 8, 10] if key_minor else [0, 2, 4, 5, 7, 9, 11]
    return [root_midi + i for i in intervals]


def _resolve_chord_tones(
    key_root: str, key_minor: bool, bar: int, octave: int = 4,
) -> set[int]:
    """Return MIDI pitches for the active chord at a given bar index.

    Cycles through i-VI-III-VII, each chord lasting _CHORD_BARS.
    Includes chord tones across ±1 octave range for flexible voicing.
    """
    tonic_midi = _midi_for_name(key_root, octave)
    chord_idx = (bar // _CHORD_BARS) % len(_CHORD_PROGRESSION)
    offset, quality = _CHORD_PROGRESSION[chord_idx]
    root = tonic_midi + offset

    if quality == "minor":
        intervals = [0, 3, 7]
    else:
        intervals = [0, 4, 7]

    tones: set[int] = set()
    # Constrain to single octave (oct_shift=0 only) to keep melodies coherent.
    # Expanding to ±1 octave creates 2+ octave jumps in the arch contour.
    for iv in intervals:
        tones.add(root + iv)
    return tones


def _resolve_tension_notes(
    key_root: str, key_minor: bool, octave: int = 4,
) -> tuple[int, int]:
    """Return (V, VII) MIDI pitches for call-resolution scheme."""
    tonic_midi = _midi_for_name(key_root, octave)
    v_pitch = tonic_midi + 7  # perfect fifth
    # VII: minor = whole step below tonic, major = half step below
    vii_pitch = tonic_midi + (10 if key_minor else 11)
    return v_pitch, vii_pitch


def _resolve_tonic(key_root: str, octave: int = 4) -> int:
    """Return tonic MIDI pitch."""
    return _midi_for_name(key_root, octave)


def _quantize_to_scale(pitch: int, scale_notes: list[int]) -> int:
    """Snap pitch to the nearest scale note (by semitone distance)."""
    if not scale_notes:
        return pitch
    return min(scale_notes, key=lambda s: abs(s - pitch))


# ---------------------------------------------------------------------------
# Style-specific motif builders
# ---------------------------------------------------------------------------

def _build_hook(
    key_root: str, key_minor: bool, bars: int, rng: random.Random,
) -> list[MidiNote]:
    """Hook style: arch contour, chord tones on quarter-beat positions.

    Each bar gets 4 chord-tone notes forming a low→mid→high→mid arch,
    plus an optional off-beat ghost note for reggaeton feel.
    All quarter-position notes are chord tones (100% ratio).

    Across bars, the peak pitch shifts to follow the chord progression,
    creating a natural melodic arc.
    """
    scale_oct4 = _get_pentatonic(key_root, key_minor, 4)
    scale_oct5 = _get_pentatonic(key_root, key_minor, 5)
    all_scale = sorted(set(scale_oct4 + scale_oct5))

    notes: list[MidiNote] = []

    for bar in range(bars):
        chord_tones = _resolve_chord_tones(key_root, key_minor, bar, 4)
        # Filter to chord tones that align with the scale
        chord_in_scale = sorted(set(
            p for p in chord_tones
            if any(abs(p % 12 - s % 12) == 0 for s in all_scale)
        ))
        if len(chord_in_scale) < 4:
            # Not enough scale-aligned chord tones; use all chord tones
            chord_in_scale = sorted(chord_tones)

        bar_start = bar * 4.0

        # Build arch: pick 4 distinct chord-tone pitches
        # low → mid-low → high → mid-high (arch shape)
        n = len(chord_in_scale)
        if n >= 4:
            low = chord_in_scale[0]
            mid_low = chord_in_scale[n // 3]
            high = chord_in_scale[-1]
            mid_high = chord_in_scale[2 * n // 3]
        elif n == 3:
            low, mid_low, high = chord_in_scale[0], chord_in_scale[1], chord_in_scale[2]
            mid_high = chord_in_scale[1]  # reuse mid
        elif n == 2:
            low = chord_in_scale[0]
            mid_low = chord_in_scale[1]
            high = chord_in_scale[0] + 12  # octave up
            mid_high = chord_in_scale[1]
        else:
            low = mid_low = high = mid_high = chord_in_scale[0]

        # Ensure high > low for true arch shape
        if high <= low + 2:
            high = low + 12 if low + 12 in chord_tones else high + 7
        if mid_high <= mid_low:
            mid_high = mid_low + 5

        contour = [
            (0.0, low, 0.75),       # beat 1 — low chord tone
            (1.0, mid_low, 0.5),    # beat 2 — walking up
            (2.0, high, 0.75),      # beat 3 — peak (chord tone)
            (3.0, mid_high, 0.5),   # beat 4 — coming down
        ]

        for pos, pitch, dur in contour:
            velocity = 85 if pos in (0.0, 2.0) else 70
            notes.append(MidiNote(
                pitch=pitch,
                start=bar_start + pos,
                duration=dur,
                velocity=velocity,
            ))

        # Ghost note on beat 2.5 (reggaeton syncopation) — deterministically varied
        if rng.random() < 0.6:
            ghost_pitch = _quantize_to_scale(
                mid_low + rng.choice([-2, 0, 2, 5]), all_scale,
            )
            notes.append(MidiNote(
                pitch=ghost_pitch,
                start=bar_start + 1.5,
                duration=0.25,
                velocity=rng.randint(45, 60),
            ))

    # Sort by start time so contour notes come before ghost notes
    notes.sort(key=lambda n: n.start)
    return notes


def _build_stabs(
    key_root: str, key_minor: bool, bars: int, rng: random.Random,
) -> list[MidiNote]:
    """Stabs style: short 16th-duration hits on dembow grid positions.

    All notes are on [1.0, 2.5, 3.0, 3.5] per bar with duration ≤ 0.25.
    Strong positions (2.5, 3.5) favor chord tones.
    """
    scale_oct4 = _get_pentatonic(key_root, key_minor, 4)
    scale_oct5 = _get_pentatonic(key_root, key_minor, 5)
    all_scale = sorted(set(scale_oct4 + scale_oct5))

    notes: list[MidiNote] = []

    for bar in range(bars):
        chord_tones = _resolve_chord_tones(key_root, key_minor, bar, 4)
        chord_in_scale = sorted(set(
            p for p in chord_tones
            if any(abs(p % 12 - s % 12) == 0 for s in all_scale)
        ))
        if not chord_in_scale:
            chord_in_scale = sorted(chord_tones)
        width = len(chord_in_scale)

        bar_start = bar * 4.0
        for pos_offset in _DEMBOW_POSITIONS:
            is_strong = pos_offset in (2.5, 3.5)

            if is_strong and width > 0:
                idx = rng.randint(0, width - 1)
                idx = ((bar + int(pos_offset * 4)) * 7 + 3) % width  # deterministic
                pitch = chord_in_scale[idx]
            else:
                pitch = rng.choice(all_scale)

            notes.append(MidiNote(
                pitch=pitch,
                start=bar_start + pos_offset,
                duration=0.25,  # 16th note
                velocity=80 if is_strong else 65,
            ))

    return notes


def _build_smooth(
    key_root: str, key_minor: bool, bars: int, rng: random.Random,
) -> list[MidiNote]:
    """Smooth style: stepwise scalar eighth-note motion.

    Uses the full diatonic scale (not pentatonic) so consecutive
    notes differ by ≤ 2 semitones. Melody walks through the scale
    with occasional direction changes for contour.
    """
    scale_oct4 = _get_diatonic(key_root, key_minor, 4)
    scale_oct5 = _get_diatonic(key_root, key_minor, 5)
    scale = sorted(set(scale_oct4 + scale_oct5))

    if not scale:
        return []

    notes: list[MidiNote] = []
    total_eighths = bars * 8

    # Start near tonic with seed-dependent offset for variation
    tonic = _resolve_tonic(key_root, 4)
    tonic_candidates = [s for s in scale if abs(s - tonic) <= 4]
    if tonic_candidates:
        current = rng.choice(tonic_candidates) if len(tonic_candidates) > 1 else tonic_candidates[0]
    else:
        current = tonic
    direction = 1  # 1 = ascending, -1 = descending

    for ei in range(total_eighths):
        pos = ei * 0.5
        bar = ei // 8

        # Strong beats: bias toward chord tones
        is_quarter = (ei % 4 == 0)
        chord_tones = _resolve_chord_tones(key_root, key_minor, bar, 4)

        # Find candidates: scale notes within ±2 semitones of current
        candidates = [s for s in scale if abs(s - current) <= 2 and s != current]

        if not candidates:
            # No valid stepwise candidate; stay or jump small
            candidates = [s for s in scale if abs(s - current) <= 3]

        if not candidates:
            candidates = [current]

        # On quarter beats, prefer chord tones that are also stepwise-valid
        if is_quarter:
            chord_candidates = [c for c in candidates if c in chord_tones]
            if chord_candidates:
                candidates = chord_candidates

        # Occasionally reverse direction for seed-dependent variation
        if rng.random() < 0.15:
            direction *= -1

        # Pick: prefer candidates in the current direction
        if direction > 0:
            up_candidates = [c for c in candidates if c > current]
            if up_candidates:
                next_pitch = rng.choice(up_candidates) if len(up_candidates) > 1 else up_candidates[0]
            else:
                down_candidates = [c for c in candidates if c < current]
                if down_candidates:
                    next_pitch = rng.choice(down_candidates) if len(down_candidates) > 1 else down_candidates[-1]
                else:
                    next_pitch = rng.choice(candidates) if len(candidates) > 1 else candidates[0]
                direction = -1
        else:
            down_candidates = [c for c in candidates if c < current]
            if down_candidates:
                next_pitch = rng.choice(down_candidates) if len(down_candidates) > 1 else down_candidates[-1]
            else:
                up_candidates = [c for c in candidates if c > current]
                if up_candidates:
                    next_pitch = rng.choice(up_candidates) if len(up_candidates) > 1 else up_candidates[0]
                else:
                    next_pitch = rng.choice(candidates) if len(candidates) > 1 else candidates[0]
                direction = 1

        # Clamp within overall range (don't go too high or low)
        if next_pitch > scale[-3]:
            direction = -1
        elif next_pitch < scale[3]:
            direction = 1

        velocity = 75 if is_quarter else 60
        notes.append(MidiNote(
            pitch=next_pitch,
            start=pos,
            duration=0.5,
            velocity=velocity,
        ))
        current = next_pitch

    return notes


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def build_motif(
    key_root: str,
    key_minor: bool,
    style: str,
    bars: int = 4,
    seed: int = 42,
) -> list[MidiNote]:
    """Generate a repeating motif using chord-aware scale selection.

    Args:
        key_root: Tonic note name (e.g. "A", "D", "F#").
        key_minor: True for minor key, False for major.
        style: One of "hook", "stabs", "smooth".
        bars: Number of bars (2-8, clamped if outside).
        seed: RNG seed for deterministic output.

    Returns:
        List of MidiNote objects.

    Raises:
        ValueError: If style is not one of "hook", "stabs", "smooth".
    """
    valid = ("hook", "stabs", "smooth")
    if style not in valid:
        raise ValueError(
            f"Invalid style '{style}'. Valid styles: {', '.join(valid)}"
        )

    # Clamp bars to safe range
    bars = max(2, min(8, bars))

    rng = random.Random(seed)

    if style == "hook":
        return _build_hook(key_root, key_minor, bars, rng)
    elif style == "stabs":
        return _build_stabs(key_root, key_minor, bars, rng)
    else:
        return _build_smooth(key_root, key_minor, bars, rng)


def apply_variation(
    motif: list[MidiNote],
    shift_beats: float = 0.0,
    transpose_semitones: int = 0,
) -> list[MidiNote]:
    """Apply rhythmic shift and/or pitch transpose to a motif.

    Returns a new list; the original motif is unchanged.
    All inter-onset intervals and durations are preserved.

    Args:
        motif: Source notes.
        shift_beats: Beat offset added to all start times.
        transpose_semitones: Semitone offset added to all pitches.

    Returns:
        New list of MidiNote objects.
    """
    return [
        MidiNote(
            pitch=note.pitch + transpose_semitones,
            start=note.start + shift_beats,
            duration=note.duration,
            velocity=note.velocity,
        )
        for note in motif
    ]


def build_call_response(
    motif: list[MidiNote],
    bars: int = 8,
    key_root: str = "A",
    key_minor: bool = True,
    seed: int = 42,
) -> list[MidiNote]:
    """Build call-and-response structure from a motif.

    First half (call): motif repeated, last note forced to V or VII (tension).
    Second half (response): motif repeated, last note forced to tonic i (resolution).

    The motif is repeated as needed to fill the section.

    Args:
        motif: Source motif notes.
        bars: Total bars for the call-response section.
        key_root: Tonic note name.
        key_minor: True for minor key.
        seed: RNG seed.

    Returns:
        List of MidiNote objects spanning `bars` worth of beats.
    """
    if not motif:
        return []

    rng = random.Random(seed)
    half_bars = bars // 2
    total_beats = bars * 4.0
    half_beats = half_bars * 4.0

    # Determine motif length in beats (end of last note)
    motif_end = max(n.start + n.duration for n in motif)
    motif_bars = max(1, int((motif_end + 3.999) / 4.0))

    repeats_per_half = max(1, half_bars // motif_bars)

    v_tonic, vii_tonic = _resolve_tension_notes(key_root, key_minor, 4)
    tonic = _resolve_tonic(key_root, 4)

    result: list[MidiNote] = []

    # --- Call half ---
    for r in range(repeats_per_half):
        offset = r * motif_bars * 4.0
        for note in motif:
            new_start = note.start + offset
            if new_start >= half_beats:
                continue  # skip notes beyond call half
            result.append(MidiNote(
                pitch=note.pitch,
                start=new_start,
                duration=note.duration,
                velocity=note.velocity,
            ))

    # Sort by start time so last-by-position = last in time
    result.sort(key=lambda n: n.start)

    # Force last note of call half to tension pitch
    call_notes = [n for n in result if n.start < half_beats]
    if call_notes:
        tension_pitch = v_tonic if rng.random() < 0.5 else vii_tonic
        call_notes[-1].pitch = tension_pitch

    # --- Response half ---
    response_start = half_beats
    for r in range(repeats_per_half):
        offset = response_start + r * motif_bars * 4.0
        for note in motif:
            new_start = note.start + offset
            if new_start >= total_beats:
                continue  # skip notes beyond section
            result.append(MidiNote(
                pitch=note.pitch,
                start=new_start,
                duration=note.duration,
                velocity=min(127, note.velocity + 5),
            ))

    # Sort again after adding response notes
    result.sort(key=lambda n: n.start)

    # Force last note to tonic
    response_notes = [n for n in result if n.start >= response_start]
    if response_notes:
        response_notes[-1].pitch = tonic

    return result
