"""Variation engine — generates unique SongDefinition instances from a seed.

Pure functions: no file I/O, no print statements. The only side effect is
the deterministic randomness from ``random.Random(idx)`` — same seed always
produces the same output.

Usage::

    from src.composer.variation import generate_variant, generate_batch

    one_song = generate_variant(42)
    fifty    = generate_batch(50)
"""

from __future__ import annotations

import random
from pathlib import Path
from typing import Iterator

from ..core.schema import (
    ArrangementItemDef,
    ArrangementTrack,
    PatternDef,
    SongDefinition,
    SongMeta,
)

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

BPMS: list[int] = [88, 90, 92, 94, 95, 96, 98, 100, 102]

KEYS_MINOR: list[str] = ["Am", "Dm", "Em", "Gm", "Bm", "Cm", "Fm"]
KEYS_MAJOR: list[str] = ["C", "F", "G", "D", "A"]
ALL_KEYS: list[str]   = KEYS_MINOR + KEYS_MAJOR

PROGRESSIONS: list[str] = [
    "i-VII-VI-VII",       # Am-G-F-G (classic)
    "i-iv-VII-III",       # Am-Dm-G-C
    "i-VI-III-VII",       # Am-F-C-G
    "i-VII-III-VI",       # Am-G-C-F
    "I-V-vi-IV",          # C-G-Am-F (major mode)
    "I-IV-V-I",           # classic major
    "i-III-VII-VI",       # minor dreamy
    "i-v-iv-VII",         # dark minor
    "I-vi-IV-V",          # 50s progression
    "i-VII-VI-iv",        # modern dark
    "i-VI-VII-i",         # loop
    "i-iv-i-VII",         # minimal
    "I-II-vi-V",          # modern major
    "i-III-VI-VII",       # uplift
    "vi-IV-I-V",          # axis
]

TITLE_PREFIXES: list[str] = [
    "Zona", "Barrio", "Calle", "Noche", "Fuego",
    "Ritmo", "Poder", "Flow", "Vibra", "Cuerpo",
]
TITLE_SUFFIXES: list[str] = [
    "Caliente", "Oscura", "Sin Fin", "Total", "Real",
    "Fatal", "Natural", "Del Party", "Con Flow", "Urbano",
]

# ---------------------------------------------------------------------------
# Variation axis parameters
# ---------------------------------------------------------------------------

DENSITY_LEVELS:  list[float] = [0.6, 0.75, 1.0]
VEL_MULT_LEVELS: list[float] = [0.85, 1.0, 1.1]
SECTION_REPEATS: list[int]   = [1, 2]  # verse/chorus repeat multiplier

SAMPLES_MAP: dict[str, str] = {
    "kick":  "kick.wav",
    "snare": "snare.wav",
    "rim":   "rim.wav",
    "clap":  "clap.wav",
    "hihat": "hihat.wav",
    "perc1": "perc1.wav",
    "perc2": "perc2.wav",
}

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


def _make_title(rng: random.Random) -> str:
    """Combine a random prefix + suffix into a song title."""
    return f"{rng.choice(TITLE_PREFIXES)} {rng.choice(TITLE_SUFFIXES)}"


def _base_tracks() -> list[ArrangementTrack]:
    """Fixed arrangement track layout — 5 tracks, same for all variants."""
    return [
        ArrangementTrack(index=1, name="Kick"),
        ArrangementTrack(index=2, name="Snare"),
        ArrangementTrack(index=3, name="Hihat"),
        ArrangementTrack(index=4, name="Clap/Rim"),
        ArrangementTrack(index=5, name="Perc"),
    ]


def _build_patterns(rng: random.Random) -> list[PatternDef]:
    """Build 9 base patterns with per-pattern randomized density and velocity_mult.

    Pattern ids, names, instruments, channels, and generators are fixed —
    matching the reggaeton_template.json structure.  The variation axes
    (density, velocity_mult) are randomized per pattern.
    """
    # (id, name, instrument, channel, bars, generator)
    base: list[tuple[int, str, str, int, int, str]] = [
        (1, "Kick Main",   "kick",  11, 8, "kick_main_notes"),
        (2, "Snare Verse", "snare", 12, 8, "snare_verse_notes"),
        (3, "Hihat 16th",  "hihat", 15, 8, "hihat_16th_notes"),
        (4, "Clap 2-4",    "clap",  16, 8, "clap_24_notes"),
        (5, "Perc Combo",  "perc2", 14, 8, "perc_combo_notes"),
        (6, "Kick Sparse", "kick",  11, 8, "kick_sparse_notes"),
        (7, "Hihat 8th",   "hihat", 15, 8, "hihat_8th_notes"),
        (8, "Rim Build",   "rim",   13, 4, "rim_build_notes"),
        (9, "Kick Outro",  "kick",  11, 8, "kick_outro_notes"),
    ]
    patterns: list[PatternDef] = []
    for pid, name, inst, ch, bars, gen in base:
        patterns.append(
            PatternDef(
                id=pid,
                name=name,
                instrument=inst,
                channel=ch,
                bars=bars,
                generator=gen,
                velocity_mult=rng.choice(VEL_MULT_LEVELS),
                density=rng.choice(DENSITY_LEVELS),
            )
        )
    return patterns


def _build_arrangement(
    rng: random.Random,
    patterns: list[PatternDef],  # noqa: ARG001 – reserved for future use
) -> list[ArrangementItemDef]:
    """Build arrangement items with variable verse/chorus lengths and optional
    breakdown.

    Structure::

        INTRO       (4 bars)  — kick_sparse + hihat_8th
        VERSE       (8|16)    — kick_main + snare + hihat_16th + perc_combo
        PRE-CHORUS  (4 bars)  — above + rim_build
        CHORUS      (8|16)    — kick_main + snare + hihat_16th + clap_24 + perc_combo
        [VERSE 2 + PRE-CHORUS 2 + CHORUS 2]
        [BREAKDOWN  (8 bars, 50% chance)] — hihat_8th + kick_sparse
        OUTRO       (8 bars)  — kick_outro + snare + hihat_16th + clap_24
    """
    items: list[ArrangementItemDef] = []
    cursor: float = 0.0

    verse_bars: int  = 8 * rng.choice(SECTION_REPEATS)
    chorus_bars: int = 8 * rng.choice(SECTION_REPEATS)
    has_breakdown: bool = rng.random() < 0.5

    def add(pattern_id: int, track: int, length: float) -> None:
        """Append one arrangement item at the current cursor (no advance)."""
        items.append(
            ArrangementItemDef(
                pattern=pattern_id,
                bar=cursor,
                bars=length,
                track=track,
            )
        )

    # --- INTRO (4 bars) ---
    add(6, 1, 4)   # kick_sparse on Kick
    add(7, 3, 4)   # hihat_8th on Hihat
    cursor += 4

    # --- VERSE / PRE-CHORUS / CHORUS × 2 ---
    for _ in range(2):
        # VERSE
        add(1, 1, verse_bars)   # kick_main
        add(2, 2, verse_bars)   # snare_verse
        add(3, 3, verse_bars)   # hihat_16th
        add(5, 5, verse_bars)   # perc_combo
        cursor += verse_bars

        # PRE-CHORUS (4 bars)
        add(1, 1, 4)   # kick_main
        add(2, 2, 4)   # snare_verse
        add(3, 3, 4)   # hihat_16th
        add(5, 5, 4)   # perc_combo
        add(8, 4, 4)   # rim_build on Clap/Rim
        cursor += 4

        # CHORUS
        add(1, 1, chorus_bars)  # kick_main
        add(2, 2, chorus_bars)  # snare_verse
        add(3, 3, chorus_bars)  # hihat_16th
        add(4, 4, chorus_bars)  # clap_24
        add(5, 5, chorus_bars)  # perc_combo
        cursor += chorus_bars

    # --- BREAKDOWN (optional, 8 bars) ---
    if has_breakdown:
        add(6, 1, 8)   # kick_sparse
        add(7, 3, 8)   # hihat_8th
        cursor += 8

    # --- OUTRO (8 bars) ---
    add(9, 1, 8)   # kick_outro on Kick
    add(2, 2, 8)   # snare_verse on Snare
    add(3, 3, 8)   # hihat_16th on Hihat
    add(4, 4, 8)   # clap_24 on Clap/Rim
    cursor += 8

    return items


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


def generate_variant(idx: int) -> SongDefinition:
    """Generate a unique ``SongDefinition`` from integer seed *idx*.

    Uses ``random.Random(idx)`` for full reproducibility.
    Same ``idx`` → same output, always.

    Varies:
    - BPM (from ``BPMS``)
    - Key (from ``ALL_KEYS``)
    - Progression name (from ``PROGRESSIONS``)
    - Title (random prefix + suffix)
    - Pattern density (per pattern, from ``DENSITY_LEVELS``)
    - Pattern velocity_mult (per pattern, from ``VEL_MULT_LEVELS``)
    - Verse/chorus bar count (8 or 16 bars)
    - Whether breakdown is included (50 % chance)

    Uniqueness key: ``(bpm, key, progression_name)`` — checked externally
    by ``generate_batch``.
    """
    rng = random.Random(idx)

    bpm:  int  = rng.choice(BPMS)
    key:  str  = rng.choice(ALL_KEYS)
    prog: str  = rng.choice(PROGRESSIONS)
    title: str = _make_title(rng)

    meta = SongMeta(bpm=bpm, key=key, title=title)

    patterns: list[PatternDef]      = _build_patterns(rng)
    tracks:  list[ArrangementTrack] = _base_tracks()
    items:   list[ArrangementItemDef] = _build_arrangement(rng, patterns)

    return SongDefinition(
        meta=meta,
        samples=SAMPLES_MAP.copy(),
        patterns=patterns,
        tracks=tracks,
        items=items,
        progression_name=prog,
        section_template="standard",
    )


def generate_batch(
    count: int = 50,
    max_attempts: int = 1000,
) -> list[SongDefinition]:
    """Generate *count* unique songs (unique on bpm+key+progression triple).

    Iterates seeds 0 … *max_attempts* until *count* unique songs are found.
    Raises ``RuntimeError`` if not enough unique combos are found.
    """
    seen: set[tuple[int, str, str]] = set()
    songs: list[SongDefinition] = []

    for seed in range(max_attempts):
        song = generate_variant(seed)
        uniq = (song.meta.bpm, song.meta.key, song.progression_name)
        if uniq not in seen:
            seen.add(uniq)
            songs.append(song)
        if len(songs) >= count:
            break

    if len(songs) < count:
        raise RuntimeError(
            f"Only found {len(songs)} unique songs in {max_attempts} attempts"
        )

    return songs
