"""
set_generator.py - ARC 3: Dynamic Set Construction & Phrasing (T041-T060)

Sistema completo de generación de sets DJ con:
- Templates de sets (1hr/2hr/4hr)
- Curvas de energía (Ramp up/Mountain/Rollercoaster)
- Algoritmo de selección de tracks
- Section tagging engine
- Hot cue generation
- Fast-mixing y Long-blend modes
- Set coherence engine v2
- Banger detection
- Warm-up set logic
- Request injection
- Memory/history check
- Genre-fluid transitions
- Drum fill injection
- Crowd noise overlay
- Continuous arrangement
- Transition type randomizer
- Drop swap
- BPM anchor points

Tareas implementadas:
T041: Setup Template Construction (1hr/2hr/4hr sets)
T042: Energy Curve Definition (Ramp up/Mountain/Rollercoaster)
T043: Track Selection Algorithm (library indexing)
T044: Section Tagging Engine ([Intro]/[Verse]/[Build]/[Drop]/[Break]/[Outro])
T045: Hot Cue Generation (auto locators at phrasing)
T046: Fast-Mixing Mode (32 bars per track)
T047: Long-Blend Mode (2-minute overlays)
T048: Set Coherence Engine v2 (strict phrasing)
T049: "Banger" Detection (energy > 8 reserve)
T050: Warm-up Set Logic (energy < 6 first 30mins)
T051: Request Injection (user "must play" track)
T052: Memory/History Check (no repeats)
T053: Genre-Fluid Transitions (125BPM→140BPM)
T054: Drum Fill Injection (custom MIDI fills)
T055: Crowd Noise Overlay (auto cheers at drops)
T056: Continuous Arrangement (stitch generations)
T057: Transition Type Randomizer (probabilistic model)
T058: Drop Swap (stitch B drop after A build)
T059: BPM Anchor Points (dynamic BPM changes)
T060: Integration Test - 30-min "Mountain" set
"""

import os
import json
import random
import logging
import hashlib
from pathlib import Path
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Set, Tuple, Union, Callable
from datetime import datetime, timedelta
from collections import defaultdict

# Importar módulos existentes del proyecto
try:
    from song_generator import SongGenerator, GENRE_CONFIGS, SECTION_BLUEPRINTS
except ImportError:
    SongGenerator = None
    GENRE_CONFIGS = {}
    SECTION_BLUEPRINTS = {}

try:
    from sample_selector import SampleSelector
except ImportError:
    SampleSelector = None

logger = logging.getLogger("SetGenerator")

# =============================================================================
# T041: SET TEMPLATES (1hr/2hr/4hr)
# =============================================================================

@dataclass
class SetTemplate:
    """Template para configuración de set DJ."""
    name: str
    duration_hours: float
    duration_beats: int
    num_tracks: int
    avg_track_length_bars: int
    transition_style: str  # 'fast', 'long_blend', 'standard'
    energy_curve_type: str  # 'ramp_up', 'mountain', 'rollercoaster', 'plateau'
    bpm_range: Tuple[int, int]
    description: str
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            'name': self.name,
            'duration_hours': self.duration_hours,
            'duration_beats': self.duration_beats,
            'num_tracks': self.num_tracks,
            'avg_track_length_bars': self.avg_track_length_bars,
            'transition_style': self.transition_style,
            'energy_curve_type': self.energy_curve_type,
            'bpm_range': self.bpm_range,
            'description': self.description
        }

# Templates predefinidos para diferentes duraciones
SET_TEMPLATES = {
    '1hr_peak_time': SetTemplate(
        name="1hr Peak Time Set",
        duration_hours=1.0,
        duration_beats=2400,  # 60 min * 4 beats/sec * 60 sec/min / (assume 126 BPM, 4/4)
        num_tracks=12,
        avg_track_length_bars=64,
        transition_style='standard',
        energy_curve_type='plateau',
        bpm_range=(126, 132),
        description="High-energy 1-hour set for peak time DJ slots"
    ),
    '2hr_standard': SetTemplate(
        name="2hr Standard Set",
        duration_hours=2.0,
        duration_beats=5760,
        num_tracks=20,
        avg_track_length_bars=80,
        transition_style='standard',
        energy_curve_type='mountain',
        bpm_range=(122, 130),
        description="Standard 2-hour club set with energy arc"
    ),
    '2hr_progressive': SetTemplate(
        name="2hr Progressive Journey",
        duration_hours=2.0,
        duration_beats=5760,
        num_tracks=18,
        avg_track_length_bars=96,
        transition_style='long_blend',
        energy_curve_type='ramp_up',
        bpm_range=(120, 128),
        description="Progressive build from deep to peak"
    ),
    '4hr_marathon': SetTemplate(
        name="4hr Marathon Set",
        duration_hours=4.0,
        duration_beats=11520,
        num_tracks=35,
        avg_track_length_bars=72,
        transition_style='long_blend',
        energy_curve_type='rollercoaster',
        bpm_range=(118, 134),
        description="Full night marathon with multiple peaks and valleys"
    ),
    '30min_showcase': SetTemplate(
        name="30min Showcase Set",
        duration_hours=0.5,
        duration_beats=960,
        num_tracks=6,
        avg_track_length_bars=48,
        transition_style='fast',
        energy_curve_type='mountain',
        bpm_range=(128, 138),
        description="Short high-impact showcase set"
    ),
    'warmup_90min': SetTemplate(
        name="90min Warm-up Set",
        duration_hours=1.5,
        duration_beats=3600,
        num_tracks=15,
        avg_track_length_bars=56,
        transition_style='long_blend',
        energy_curve_type='ramp_up',
        bpm_range=(118, 126),
        description="Warm-up set building energy gradually"
    ),
}

# T046: Fast-mixing mode
FAST_MIXING_CONFIG = {
    'bars_per_track': 32,
    'transition_bars': 8,
    'overlap_bars': 16,
    'energy_boost_per_transition': 0.5,
    'description': "Quick 32-bar sections for fast energy progression"
}

# T047: Long-blend mode  
LONG_BLEND_CONFIG = {
    'min_overlay_seconds': 120,  # 2 minutes
    'bars_per_track': 96,
    'transition_bars': 32,
    'overlap_bars': 64,
    'description': "Extended overlays for smooth journey sets"
}

# =============================================================================
# T042: ENERGY CURVE DEFINITIONS
# =============================================================================

class EnergyCurve:
    """
    Define y calcula curvas de energía para sets.
    
    Tipos:
    - ramp_up: Subida gradual desde bajo a alto
    - mountain: Pico central con subida y bajada simétrica
    - rollercoaster: Múltiples picos y valles
    - plateau: Alto constante
    - valley: Comienza alto, baja, vuelve a subir
    """
    
    CURVE_TYPES = ['ramp_up', 'mountain', 'rollercoaster', 'plateau', 'valley']
    
    def __init__(self, curve_type: str, duration_beats: int, 
                 start_energy: float = 0.3, end_energy: float = 0.3,
                 peak_energy: float = 1.0, peak_position: float = 0.5):
        self.curve_type = curve_type
        self.duration_beats = duration_beats
        self.start_energy = start_energy
        self.end_energy = end_energy
        self.peak_energy = peak_energy
        self.peak_position = peak_position  # 0.0-1.0 position of peak
        
    def get_energy_at(self, beat: float) -> float:
        """Get energy level (0.0-1.0) at a specific beat position."""
        progress = min(1.0, max(0.0, beat / self.duration_beats))
        
        if self.curve_type == 'ramp_up':
            # Exponential ramp up
            return self.start_energy + (self.peak_energy - self.start_energy) * (progress ** 0.7)
            
        elif self.curve_type == 'mountain':
            # Parabolic mountain shape
            peak_dist = abs(progress - self.peak_position)
            if progress < self.peak_position:
                # Ascending
                ascent = progress / self.peak_position if self.peak_position > 0 else 1
                return self.start_energy + (self.peak_energy - self.start_energy) * ascent
            else:
                # Descending
                descent = (1 - progress) / (1 - self.peak_position) if self.peak_position < 1 else 0
                return self.end_energy + (self.peak_energy - self.end_energy) * descent
                
        elif self.curve_type == 'rollercoaster':
            # Multiple peaks using sine waves
            base = (self.start_energy + self.end_energy) / 2
            amplitude = (self.peak_energy - base) * 0.8
            wave1 = amplitude * (0.5 + 0.5 * (3.14159 * 2 * progress))
            wave2 = amplitude * 0.3 * (0.5 + 0.5 * (3.14159 * 4 * progress + 1))
            return base + wave1 + wave2
            
        elif self.curve_type == 'plateau':
            # Sustained high energy
            if progress < 0.1:
                return self.start_energy + (self.peak_energy - self.start_energy) * (progress / 0.1)
            elif progress > 0.9:
                return self.end_energy + (self.peak_energy - self.end_energy) * ((1 - progress) / 0.1)
            else:
                return self.peak_energy
                
        elif self.curve_type == 'valley':
            # Starts high, dips, returns high
            valley_depth = 0.2
            if progress < 0.5:
                return self.peak_energy - (self.peak_energy - valley_depth) * (progress * 2)
            else:
                return valley_depth + (self.peak_energy - valley_depth) * ((progress - 0.5) * 2)
                
        return 0.5
    
    def get_energy_curve(self, resolution_beats: int = 16) -> List[Dict[str, Any]]:
        """Generate energy curve points at specified resolution."""
        points = []
        num_points = self.duration_beats // resolution_beats
        
        for i in range(num_points + 1):
            beat = i * resolution_beats
            energy = self.get_energy_at(beat)
            points.append({
                'beat': beat,
                'bar': beat / 4,
                'energy': round(energy, 3),
                'position': round(beat / self.duration_beats, 3)
            })
            
        return points
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            'curve_type': self.curve_type,
            'duration_beats': self.duration_beats,
            'start_energy': self.start_energy,
            'end_energy': self.end_energy,
            'peak_energy': self.peak_energy,
            'peak_position': self.peak_position,
            'curve_data': self.get_energy_curve()
        }


# =============================================================================
# T043: TRACK SELECTION ALGORITHM
# =============================================================================

@dataclass
class TrackCandidate:
    """Candidato de track para selección en set."""
    track_id: str
    genre: str
    bpm: float
    key: str
    energy: float  # 0.0-1.0
    duration_bars: int
    sections: List[Dict[str, Any]]
    spectral_signature: Optional[Dict[str, float]] = None
    last_played: Optional[datetime] = None
    play_count: int = 0
    tags: List[str] = field(default_factory=list)
    
    def compute_compatibility_score(self, target_bpm: float, target_key: str, 
                                    target_energy: float, 
                                    played_recently: Set[str],
                                    banger_reserve: List[str]) -> float:
        """
        Calcula score de compatibilidad (0.0-1.0) para este track.
        
        Factores:
        - BPM compatibility (±3% = 1.0, ±6% = 0.7, ±10% = 0.4)
        - Key compatibility (same = 1.0, relative = 0.8, compatible = 0.6)
        - Energy match (|actual - target| < 0.1 = 1.0, < 0.2 = 0.8, etc.)
        - History penalty (played recently = -0.3)
        - Banger reserve bonus (if energy > 0.8 and in reserve)
        """
        score = 1.0
        
        # BPM compatibility
        bpm_diff_pct = abs(self.bpm - target_bpm) / target_bpm
        if bpm_diff_pct <= 0.03:
            score *= 1.0
        elif bpm_diff_pct <= 0.06:
            score *= 0.7
        elif bpm_diff_pct <= 0.10:
            score *= 0.4
        else:
            score *= 0.1
            
        # Key compatibility
        key_score = self._compute_key_compatibility(target_key)
        score *= key_score
        
        # Energy match
        energy_diff = abs(self.energy - target_energy)
        if energy_diff <= 0.1:
            score *= 1.0
        elif energy_diff <= 0.2:
            score *= 0.85
        elif energy_diff <= 0.3:
            score *= 0.7
        else:
            score *= 0.5
            
        # History penalty
        if self.track_id in played_recently:
            score *= 0.4
        elif self.play_count > 0:
            # Reduce score based on play count (fatigue)
            score *= max(0.3, 1.0 - (self.play_count * 0.15))
            
        # Banger reserve bonus (T049)
        if self.energy > 0.8 and self.track_id in banger_reserve:
            score *= 1.2  # Boost for reserved bangers
            
        return min(1.0, score)
    
    def _compute_key_compatibility(self, target_key: str) -> float:
        """Compute key compatibility score (0.0-1.0)."""
        # Circle of fifths compatibility
        key_circle = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'D#', 'A#', 'F']
        
        # Parse keys (handle minor 'm' suffix)
        self_root = self.key.replace('m', '').replace('#', '')
        target_root = target_key.replace('m', '').replace('#', '')
        
        is_self_minor = 'm' in self.key
        is_target_minor = 'm' in target_key
        
        # Same key
        if self.key == target_key:
            return 1.0
            
        # Relative major/minor
        relative_majors = {'Am': 'C', 'Em': 'G', 'Dm': 'F', 'Bm': 'D', 'F#m': 'A', 'C#m': 'E',
                          'G#m': 'B', 'D#m': 'C#', 'A#m': 'D#', 'Fm': 'G#', 'Cm': 'D#', 'Gm': 'A#'}
        relative_minors = {v: k for k, v in relative_majors.items()}
        
        if is_self_minor and not is_target_minor:
            if relative_majors.get(self.key) == target_key:
                return 0.85
        elif not is_self_minor and is_target_minor:
            if relative_minors.get(self.key) == target_key:
                return 0.85
                
        # Same root (mode change)
        if self_root == target_root:
            return 0.75
            
        # Perfect fourth/fifth
        try:
            self_idx = key_circle.index(self_root)
            target_idx = key_circle.index(target_root)
            distance = min(abs(self_idx - target_idx), 12 - abs(self_idx - target_idx))
            
            if distance == 1:  # Fifth/fourth
                return 0.7
            elif distance == 2:  # Second/seventh
                return 0.5
        except ValueError:
            pass
            
        return 0.3
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            'track_id': self.track_id,
            'genre': self.genre,
            'bpm': self.bpm,
            'key': self.key,
            'energy': self.energy,
            'duration_bars': self.duration_bars,
            'sections': self.sections,
            'play_count': self.play_count,
            'tags': self.tags,
            'last_played': self.last_played.isoformat() if self.last_played else None
        }


class TrackLibrary:
    """
    T043: Biblioteca indexada de tracks para selección.
    
    Features:
    - Indexación por BPM, key, género, energía
    - Historial de reproducción
    - Sistema de tags
    - Búsqueda por características espectrales
    """
    
    def __init__(self, library_path: Optional[str] = None):
        self.library_path = library_path or "librerias/all_tracks"
        self.tracks: Dict[str, TrackCandidate] = {}
        self.index_by_genre: Dict[str, Set[str]] = defaultdict(set)
        self.index_by_key: Dict[str, Set[str]] = defaultdict(set)
        self.index_by_bpm_range: Dict[Tuple[int, int], Set[str]] = defaultdict(set)
        self.index_by_energy: Dict[str, Set[str]] = {
            'low': set(), 'medium': set(), 'high': set(), 'banger': set()
        }
        self.play_history: List[Dict[str, Any]] = []
        self._banger_reserve: Set[str] = set()  # T049
        
    @property
    def total_tracks(self) -> int:
        """Get total number of tracks in library."""
        return len(self.tracks)
        
    def add_track(self, track: TrackCandidate) -> None:
        """Add track to library with indexing."""
        self.tracks[track.track_id] = track
        
        # Index by genre
        self.index_by_genre[track.genre].add(track.track_id)
        
        # Index by key
        self.index_by_key[track.key].add(track.track_id)
        
        # Index by BPM range (round to nearest 5)
        bpm_lower = int(track.bpm // 5) * 5
        self.index_by_bpm_range[(bpm_lower, bpm_lower + 5)].add(track.track_id)
        
        # Index by energy
        if track.energy >= 0.85:
            self.index_by_energy['banger'].add(track.track_id)
            self._banger_reserve.add(track.track_id)  # T049: Auto-add high energy to reserve
        elif track.energy >= 0.7:
            self.index_by_energy['high'].add(track.track_id)
        elif track.energy >= 0.4:
            self.index_by_energy['medium'].add(track.track_id)
        else:
            self.index_by_energy['low'].add(track.track_id)
            
    def select_tracks_for_set(self, template: SetTemplate, energy_curve: EnergyCurve,
                              start_genre: str = 'tech-house',
                              user_requests: Optional[List[str]] = None) -> List[TrackCandidate]:
        """
        T043: Select optimal track sequence for a set.
        
        Args:
            template: Set template configuration
            energy_curve: Energy curve to follow
            start_genre: Starting genre
            user_requests: T051: List of track IDs user requested (must play)
            
        Returns:
            List of TrackCandidate in optimal order
        """
        selected = []
        current_bpm = (template.bpm_range[0] + template.bpm_range[1]) / 2
        current_key = 'Am'
        current_genre = start_genre
        
        # T051: Handle user requests (must play tracks)
        request_tracks = []
        if user_requests:
            for req_id in user_requests:
                if req_id in self.tracks:
                    request_tracks.append(self.tracks[req_id])
                    
        # Get recently played tracks (T052: Memory check)
        played_recently = set()
        for play in self.play_history[-20:]:  # Last 20 plays
            played_recently.add(play['track_id'])
            
        # Calculate track positions based on template
        track_positions = []
        for i in range(template.num_tracks):
            position_beats = i * (template.duration_beats // template.num_tracks)
            target_energy = energy_curve.get_energy_at(position_beats)
            
            track_positions.append({
                'position': i,
                'start_beat': position_beats,
                'target_energy': target_energy,
                'target_bpm': current_bpm,
                'target_key': current_key
            })
            
        # Select tracks for each position
        used_track_ids = set()
        
        for i, pos in enumerate(track_positions):
            # T051: Inject user request at appropriate position
            if request_tracks and i == len(track_positions) // 2:  # Middle of set
                req_track = request_tracks.pop(0)
                if req_track.track_id not in used_track_ids:
                    selected.append(req_track)
                    used_track_ids.add(req_track.track_id)
                    current_bpm = req_track.bpm
                    current_key = req_track.key
                    current_genre = req_track.genre
                    continue
                    
            # Get candidates for this position
            candidates = self._get_candidates_for_position(
                pos['target_bpm'], pos['target_key'], 
                pos['target_energy'], current_genre,
                played_recently, self._banger_reserve
            )
            
            # Filter out used tracks
            candidates = [c for c in candidates if c.track_id not in used_track_ids]
            
            if candidates:
                # Select best candidate
                best = max(candidates, key=lambda c: c.compute_compatibility_score(
                    pos['target_bpm'], pos['target_key'], pos['target_energy'],
                    played_recently, list(self._banger_reserve)
                ))
                
                selected.append(best)
                used_track_ids.add(best.track_id)
                
                # Update current state for next transition
                current_bpm = best.bpm
                current_key = best.key
                current_genre = best.genre
                
        return selected
    
    def _get_candidates_for_position(self, target_bpm: float, target_key: str,
                                      target_energy: float, genre: str,
                                      played_recently: Set[str],
                                      banger_reserve: Set[str]) -> List[TrackCandidate]:
        """Get candidate tracks for a set position."""
        candidates = []
        
        # Determine energy category
        if target_energy >= 0.85:
            energy_cat = 'banger'
        elif target_energy >= 0.7:
            energy_cat = 'high'
        elif target_energy >= 0.4:
            energy_cat = 'medium'
        else:
            energy_cat = 'low'
            
        # Get tracks from appropriate energy category and genre
        track_ids = self.index_by_energy[energy_cat] & self.index_by_genre.get(genre, set())
        
        # If not enough, expand search
        if len(track_ids) < 5:
            track_ids = track_ids | self.index_by_genre.get(genre, set())
            
        for tid in track_ids:
            if tid in self.tracks:
                candidates.append(self.tracks[tid])
                
        return candidates
    
    def get_banger_reserve(self) -> List[str]:
        """T049: Get list of reserved banger tracks (energy > 0.8)."""
        return list(self._banger_reserve)
    
    def record_play(self, track_id: str) -> None:
        """Record track play in history."""
        self.play_history.append({
            'track_id': track_id,
            'timestamp': datetime.now().isoformat()
        })
        
        if track_id in self.tracks:
            self.tracks[track_id].play_count += 1
            self.tracks[track_id].last_played = datetime.now()
            
    def to_dict(self) -> Dict[str, Any]:
        return {
            'total_tracks': len(self.tracks),
            'by_genre': {k: len(v) for k, v in self.index_by_genre.items()},
            'by_key': {k: len(v) for k, v in self.index_by_key.items()},
            'by_energy': {k: len(v) for k, v in self.index_by_energy.items()},
            'banger_reserve': len(self._banger_reserve),
            'play_history_count': len(self.play_history)
        }


# =============================================================================
# T044: SECTION TAGGING ENGINE
# =============================================================================

@dataclass
class SectionTag:
    """Etiqueta de sección musical."""
    kind: str  # 'intro', 'verse', 'build', 'drop', 'break', 'outro', 'transition'
    start_bar: float
    end_bar: float
    energy: float
    confidence: float  # 0.0-1.0 detection confidence
    tags: List[str] = field(default_factory=list)  # Additional tags
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            'kind': self.kind,
            'start_bar': self.start_bar,
            'end_bar': self.end_bar,
            'length_bars': self.end_bar - self.start_bar,
            'energy': self.energy,
            'confidence': self.confidence,
            'tags': self.tags
        }


class SectionTaggingEngine:
    """
    T044: Engine para etiquetar secciones musicales.
    
    Heurísticas de detección:
    - Cambios de energía espectral
    - Transiciones de percusión (fills, cambios de patrón)
    - Cambios armónicos
    - Silencios y breakdowns
    """
    
    SECTION_KINDS = ['intro', 'verse', 'build', 'drop', 'break', 'outro', 'transition']
    
    # Heurísticas para detección por nombre
    NAME_PATTERNS = {
        'intro': ['intro', 'in', 'start', 'beginning'],
        'verse': ['verse', 'verso', 'main', 'groove'],
        'build': ['build', 'rise', 'up', 'rising', 'sube'],
        'drop': ['drop', 'chorus', 'hook', 'peak', 'estribillo', 'coro'],
        'break': ['break', 'breakdown', 'puente', 'bridge', 'calm'],
        'outro': ['outro', 'end', 'out', 'finish', 'ending'],
        'transition': ['transition', 'trans', 'mix', 'bridge']
    }
    
    def __init__(self):
        self.detection_rules = self._build_detection_rules()
        
    def _build_detection_rules(self) -> Dict[str, Callable]:
        """Build detection heuristics."""
        return {
            'energy_drop': self._detect_energy_drops,
            'fill_pattern': self._detect_fill_patterns,
            'harmonic_change': self._detect_harmonic_changes,
            'silence_gap': self._detect_silence_gaps,
        }
    
    def tag_sections(self, track_data: Dict[str, Any]) -> List[SectionTag]:
        """
        Tag sections in a track based on analysis.
        
        Args:
            track_data: Dict with track analysis data including:
                - duration_bars
                - energy_profile: List of energy values per bar
                - percussion_events: List of fill/drum events
                - harmonic_progression: List of chord changes
                
        Returns:
            List of SectionTag
        """
        tags = []
        duration = track_data.get('duration_bars', 64)
        
        # Apply name-based detection first
        sections_from_name = self._tag_from_names(track_data.get('section_names', []))
        tags.extend(sections_from_name)
        
        # Apply energy-based detection
        energy_profile = track_data.get('energy_profile', [])
        if energy_profile:
            tags.extend(self._tag_from_energy(energy_profile, duration))
            
        # Merge overlapping tags
        tags = self._merge_tags(tags)
        
        # Ensure full coverage
        tags = self._ensure_full_coverage(tags, duration)
        
        return sorted(tags, key=lambda t: t.start_bar)
    
    def _tag_from_names(self, section_names: List[Dict[str, Any]]) -> List[SectionTag]:
        """Tag sections based on their names."""
        tags = []
        
        for section in section_names:
            name = section.get('name', '').lower()
            start = section.get('start_bar', 0)
            end = section.get('end_bar', start + 16)
            
            # Match against patterns
            for kind, patterns in self.NAME_PATTERNS.items():
                if any(p in name for p in patterns):
                    energy = self._energy_for_kind(kind)
                    tags.append(SectionTag(
                        kind=kind,
                        start_bar=start,
                        end_bar=end,
                        energy=energy,
                        confidence=0.85,
                        tags=['name_based']
                    ))
                    break
                    
        return tags
    
    def _tag_from_energy(self, energy_profile: List[float], duration_bars: int) -> List[SectionTag]:
        """Tag sections based on energy profile."""
        tags = []
        
        # Detect significant energy changes
        threshold = 0.15
        min_section_length = 8  # bars
        
        i = 0
        while i < len(energy_profile) - min_section_length:
            current_energy = energy_profile[i]
            
            # Look for significant change
            for j in range(i + min_section_length, min(i + 64, len(energy_profile))):
                energy_diff = abs(energy_profile[j] - current_energy)
                
                if energy_diff > threshold:
                    # Found a section boundary
                    section_length = j - i
                    avg_energy = sum(energy_profile[i:j]) / section_length
                    
                    kind = self._kind_from_energy(avg_energy, i, duration_bars)
                    
                    tags.append(SectionTag(
                        kind=kind,
                        start_bar=float(i),
                        end_bar=float(j),
                        energy=avg_energy,
                        confidence=min(1.0, energy_diff * 2),
                        tags=['energy_based']
                    ))
                    
                    i = j
                    break
            else:
                i += min_section_length
                
        return tags
    
    def _energy_for_kind(self, kind: str) -> float:
        """Get typical energy for section kind."""
        energies = {
            'intro': 0.25,
            'verse': 0.55,
            'build': 0.75,
            'drop': 1.0,
            'break': 0.35,
            'outro': 0.25,
            'transition': 0.65
        }
        return energies.get(kind, 0.5)
    
    def _kind_from_energy(self, energy: float, position: int, duration: int) -> str:
        """Determine section kind from energy and position."""
        position_ratio = position / duration if duration > 0 else 0.5
        
        if position_ratio < 0.15:
            return 'intro' if energy < 0.5 else 'build'
        elif position_ratio > 0.85:
            return 'outro' if energy < 0.5 else 'build'
        elif energy > 0.8:
            return 'drop'
        elif energy > 0.6:
            return 'build' if position_ratio < 0.7 else 'verse'
        elif energy < 0.4:
            return 'break'
        else:
            return 'verse'
    
    def _detect_energy_drops(self, track_data: Dict[str, Any]) -> List[Tuple[int, int]]:
        """Detect sections with sudden energy drops."""
        # Implementation placeholder
        return []
    
    def _detect_fill_patterns(self, track_data: Dict[str, Any]) -> List[Tuple[int, int]]:
        """Detect sections with drum fills (indicating transitions)."""
        # Implementation placeholder
        return []
    
    def _detect_harmonic_changes(self, track_data: Dict[str, Any]) -> List[Tuple[int, int]]:
        """Detect sections with harmonic changes."""
        # Implementation placeholder
        return []
    
    def _detect_silence_gaps(self, track_data: Dict[str, Any]) -> List[Tuple[int, int]]:
        """Detect sections with silence gaps (breakdowns)."""
        # Implementation placeholder
        return []
    
    def _merge_tags(self, tags: List[SectionTag]) -> List[SectionTag]:
        """Merge overlapping tags with priority to high confidence."""
        if not tags:
            return tags
            
        # Sort by confidence
        sorted_tags = sorted(tags, key=lambda t: t.confidence, reverse=True)
        
        merged = []
        used_ranges = []
        
        for tag in sorted_tags:
            # Check if this tag overlaps with already used ranges
            overlaps = False
            for used_start, used_end in used_ranges:
                if not (tag.end_bar <= used_start or tag.start_bar >= used_end):
                    # Overlaps
                    overlaps = True
                    break
                    
            if not overlaps:
                merged.append(tag)
                used_ranges.append((tag.start_bar, tag.end_bar))
                
        return merged
    
    def _ensure_full_coverage(self, tags: List[SectionTag], duration_bars: int) -> List[SectionTag]:
        """Ensure the entire track duration is covered by sections."""
        if not tags:
            # Create a single section for the whole track
            return [SectionTag(
                kind='verse',
                start_bar=0.0,
                end_bar=float(duration_bars),
                energy=0.5,
                confidence=0.5,
                tags=['fallback']
            )]
            
        # Sort by position
        tags = sorted(tags, key=lambda t: t.start_bar)
        
        # Fill gaps
        complete = []
        current_pos = 0.0
        
        for tag in tags:
            if tag.start_bar > current_pos:
                # Gap detected - fill with transition
                complete.append(SectionTag(
                    kind='transition',
                    start_bar=current_pos,
                    end_bar=tag.start_bar,
                    energy=0.5,
                    confidence=0.6,
                    tags=['auto_filled_gap']
                ))
            complete.append(tag)
            current_pos = max(current_pos, tag.end_bar)
            
        # Fill end gap if needed
        if current_pos < duration_bars:
            complete.append(SectionTag(
                kind='outro' if current_pos / duration_bars > 0.8 else 'transition',
                start_bar=current_pos,
                end_bar=float(duration_bars),
                energy=0.3 if current_pos / duration_bars > 0.8 else 0.5,
                confidence=0.5,
                tags=['auto_filled_end']
            ))
            
        return complete


# =============================================================================
# T045: HOT CUE GENERATION
# =============================================================================

@dataclass
class HotCue:
    """Hot cue para navegación DJ."""
    position_beats: float
    name: str
    type: str  # 'intro', 'build', 'drop', 'break', 'outro', 'phrase'
    color: int  # Ableton color index
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            'position_beats': self.position_beats,
            'position_bars': self.position_beats / 4,
            'name': self.name,
            'type': self.type,
            'color': self.color
        }


class HotCueGenerator:
    """
    T045: Genera hot cues automáticamente en puntos de phrasing.
    
    Coloca locators en:
    - Inicio de secciones (Intro, Build, Drop, Break, Outro)
    - Cambios de frase musical (cada 16 o 32 beats)
    - Puntos de transición óptimos
    """
    
    CUE_COLORS = {
        'intro': 10,    # Red
        'build': 25,    # Light green
        'drop': 50,     # Magenta
        'break': 60,    # Purple
        'outro': 30,    # Blue
        'phrase': 58,   # Gray
    }
    
    PHRASE_LENGTH_BARS = {
        'techno': 8,
        'house': 8,
        'tech-house': 8,
        'trance': 16,
        'drum-and-bass': 16,
        'reggaeton': 4,
    }
    
    def generate_hot_cues(self, sections: List[SectionTag], genre: str = 'techno',
                         bpm: float = 126.0) -> List[HotCue]:
        """
        Generate hot cues for a track's sections.
        
        Args:
            sections: List of section tags
            genre: Genre for phrase length determination
            bpm: BPM for timing calculations
            
        Returns:
            List of HotCue
        """
        cues = []
        phrase_bars = self.PHRASE_LENGTH_BARS.get(genre, 8)
        
        # Section start cues
        for section in sections:
            cues.append(HotCue(
                position_beats=section.start_bar * 4,
                name=f"[{section.kind.upper()}]",
                type=section.kind,
                color=self.CUE_COLORS.get(section.kind, 58)
            ))
            
        # Phrase boundary cues
        max_bar = max(s.end_bar for s in sections) if sections else 64
        current_bar = 0
        
        while current_bar < max_bar:
            # Check if we already have a cue at this position
            existing = any(abs(c.position_beats - current_bar * 4) < 0.5 for c in cues)
            
            if not existing:
                # Determine section at this position
                section = self._get_section_at(sections, current_bar)
                
                cues.append(HotCue(
                    position_beats=current_bar * 4,
                    name=f"|{int(current_bar)}|",
                    type='phrase',
                    color=self.CUE_COLORS['phrase']
                ))
                
            current_bar += phrase_bars
            
        # Sort and deduplicate
        cues = sorted(cues, key=lambda c: c.position_beats)
        
        return self._deduplicate_cues(cues)
    
    def _get_section_at(self, sections: List[SectionTag], bar: float) -> Optional[SectionTag]:
        """Get the section at a specific bar position."""
        for section in sections:
            if section.start_bar <= bar < section.end_bar:
                return section
        return None
    
    def _deduplicate_cues(self, cues: List[HotCue]) -> List[HotCue]:
        """Remove duplicate cues at similar positions."""
        if not cues:
            return cues
            
        deduped = [cues[0]]
        
        for cue in cues[1:]:
            last = deduped[-1]
            if abs(cue.position_beats - last.position_beats) < 2:  # Within 2 beats
                # Keep the one with higher priority (section over phrase)
                if self._cue_priority(cue) > self._cue_priority(last):
                    deduped[-1] = cue
            else:
                deduped.append(cue)
                
        return deduped
    
    def _cue_priority(self, cue: HotCue) -> int:
        """Get priority for cue type (higher = more important)."""
        priorities = {
            'intro': 6,
            'build': 5,
            'drop': 7,
            'break': 4,
            'outro': 3,
            'phrase': 1,
        }
        return priorities.get(cue.type, 0)


# =============================================================================
# T046-T047: MIXING MODES
# =============================================================================

class MixingMode:
    """Base class for mixing modes."""
    
    def __init__(self, config: Dict[str, Any]):
        self.config = config
        
    def calculate_transition_points(self, track_a_duration: int, track_b_duration: int,
                                   track_a_sections: List[SectionTag],
                                   track_b_sections: List[SectionTag]) -> Dict[str, Any]:
        """Calculate optimal transition points between two tracks."""
        raise NotImplementedError


class FastMixingMode(MixingMode):
    """
    T046: Fast-mixing mode.
    
    - 32 bars per track
    - Quick 8-bar transitions
    - High energy progression
    """
    
    def __init__(self):
        super().__init__(FAST_MIXING_CONFIG)
        
    def calculate_transition_points(self, track_a_duration: int, track_b_duration: int,
                                   track_a_sections: List[SectionTag],
                                   track_b_sections: List[SectionTag]) -> Dict[str, Any]:
        bars_a = min(track_a_duration, self.config['bars_per_track'])
        bars_b = min(track_b_duration, self.config['bars_per_track'])
        
        # Find optimal out point in track A (prefer outro or last section)
        out_point = self._find_optimal_out_point(track_a_sections, bars_a)
        
        # Find optimal in point in track B (prefer intro or build)
        in_point = self._find_optimal_in_point(track_b_sections)
        
        overlap_bars = self.config['overlap_bars']
        transition_bars = self.config['transition_bars']
        
        return {
            'mode': 'fast_mixing',
            'track_a_out_bar': out_point,
            'track_b_in_bar': in_point,
            'overlap_bars': overlap_bars,
            'transition_start_bar': out_point - transition_bars,
            'transition_end_bar': out_point,
            'energy_boost': self.config['energy_boost_per_transition']
        }
    
    def _find_optimal_out_point(self, sections: List[SectionTag], max_bars: int) -> float:
        """Find best point to exit a track."""
        # Prefer outro section end
        for section in sections:
            if section.kind == 'outro' and section.end_bar <= max_bars:
                return section.start_bar
                
        # Fall back to last section end or max bars
        if sections:
            return min(sections[-1].end_bar, max_bars)
        return max_bars
    
    def _find_optimal_in_point(self, sections: List[SectionTag]) -> float:
        """Find best point to enter a track."""
        # Prefer intro section
        for section in sections:
            if section.kind == 'intro':
                return section.start_bar
                
        # Fall back to first section start
        if sections:
            return sections[0].start_bar
        return 0.0


class LongBlendMode(MixingMode):
    """
    T047: Long-blend mode.
    
    - 2-minute overlays minimum
    - Extended 32-bar transitions  
    - 64-bar overlaps
    - Smooth journey progression
    """
    
    def __init__(self):
        super().__init__(LONG_BLEND_CONFIG)
        
    def calculate_transition_points(self, track_a_duration: int, track_b_duration: int,
                                   track_a_sections: List[SectionTag],
                                   track_b_sections: List[SectionTag]) -> Dict[str, Any]:
        bars_a = min(track_a_duration, self.config['bars_per_track'])
        bars_b = min(track_b_duration, self.config['bars_per_track'])
        
        # Find drop in track A (keep playing through it)
        drop_section = None
        for section in track_a_sections:
            if section.kind == 'drop':
                drop_section = section
                break
                
        # Start transition from after drop or at 50%
        if drop_section:
            transition_start = drop_section.end_bar
        else:
            transition_start = bars_a * 0.5
            
        overlap_bars = self.config['overlap_bars']
        
        return {
            'mode': 'long_blend',
            'track_a_out_bar': transition_start + overlap_bars,
            'track_b_in_bar': 0,  # Start track B from beginning
            'overlap_bars': overlap_bars,
            'transition_start_bar': transition_start,
            'transition_end_bar': transition_start + overlap_bars,
            'min_overlay_seconds': self.config['min_overlay_seconds']
        }


# =============================================================================
# T048: SET COHERENCE ENGINE v2
# =============================================================================

class SetCoherenceEngine:
    """
    T048: Motor de coherencia estricta para sets.
    
    Valida:
    - Phrasing alignment (secciones en múltiplos de 8/16 barras)
    - Key compatibility entre tracks consecutivos
    - BPM transition smoothness (±3% máximo por transición)
    - Energy progression consistency
    - No repeated tracks
    """
    
    PHRASE_LENGTHS = [4, 8, 16, 32]  # Valid phrase lengths in bars
    MAX_BPM_CHANGE_PCT = 0.06  # 6% máximo cambio BPM
    MIN_KEY_COMPATIBILITY = 0.5  # Mínimo score de compatibilidad armónica
    
    def __init__(self):
        self.validation_rules = self._build_validation_rules()
        
    def validate_set(self, tracks: List[TrackCandidate], 
                    energy_curve: EnergyCurve) -> Dict[str, Any]:
        """
        Validate a complete set for coherence.
        
        Returns:
            Dict with validation results and recommendations
        """
        issues = []
        score = 1.0
        
        # Check phrasing alignment
        phrasing_issues = self._check_phrasing(tracks)
        if phrasing_issues:
            issues.extend(phrasing_issues)
            score -= 0.1 * len(phrasing_issues)
            
        # Check BPM transitions
        bpm_issues = self._check_bpm_transitions(tracks)
        if bpm_issues:
            issues.extend(bpm_issues)
            score -= 0.15 * len(bpm_issues)
            
        # Check key compatibility
        key_issues = self._check_key_compatibility(tracks)
        if key_issues:
            issues.extend(key_issues)
            score -= 0.1 * len(key_issues)
            
        # Check energy progression
        energy_issues = self._check_energy_progression(tracks, energy_curve)
        if energy_issues:
            issues.extend(energy_issues)
            score -= 0.1 * len(energy_issues)
            
        # Check for repeats
        repeat_issues = self._check_repeats(tracks)
        if repeat_issues:
            issues.extend(repeat_issues)
            score -= 0.2 * len(repeat_issues)
            
        return {
            'coherence_score': max(0.0, score),
            'valid': len(issues) == 0,
            'issues': issues,
            'total_tracks': len(tracks),
            'recommendations': self._generate_recommendations(issues)
        }
    
    def _build_validation_rules(self) -> Dict[str, Callable]:
        """Build validation rule functions."""
        return {
            'phrasing': self._check_phrasing,
            'bpm': self._check_bpm_transitions,
            'key': self._check_key_compatibility,
            'energy': self._check_energy_progression,
            'repeats': self._check_repeats,
        }
    
    def _check_phrasing(self, tracks: List[TrackCandidate]) -> List[Dict[str, Any]]:
        """Check that track boundaries align with phrase boundaries."""
        issues = []
        
        for i, track in enumerate(tracks):
            for section in track.sections:
                length = section.get('length_bars', 16)
                start = section.get('start_bar', 0)
                
                # Check if section length is a valid phrase length
                if length not in self.PHRASE_LENGTHS:
                    # Find nearest valid length
                    nearest = min(self.PHRASE_LENGTHS, key=lambda x: abs(x - length))
                    
                    issues.append({
                        'type': 'phrasing_misalignment',
                        'track': track.track_id,
                        'section': section.get('kind', 'unknown'),
                        'position': i,
                        'actual_length': length,
                        'recommended_length': nearest,
                        'severity': 'warning'
                    })
                    
        return issues
    
    def _check_bpm_transitions(self, tracks: List[TrackCandidate]) -> List[Dict[str, Any]]:
        """Check BPM changes between consecutive tracks."""
        issues = []
        
        for i in range(len(tracks) - 1):
            current_bpm = tracks[i].bpm
            next_bpm = tracks[i + 1].bpm
            
            change_pct = abs(next_bpm - current_bpm) / current_bpm
            
            if change_pct > self.MAX_BPM_CHANGE_PCT:
                issues.append({
                    'type': 'bpm_jump',
                    'position': i,
                    'track_a': tracks[i].track_id,
                    'track_b': tracks[i + 1].track_id,
                    'bpm_a': current_bpm,
                    'bpm_b': next_bpm,
                    'change_pct': round(change_pct * 100, 1),
                    'max_allowed_pct': self.MAX_BPM_CHANGE_PCT * 100,
                    'severity': 'error'
                })
            elif change_pct > 0.04:  # Warning at 4%
                issues.append({
                    'type': 'bpm_warning',
                    'position': i,
                    'track_a': tracks[i].track_id,
                    'track_b': tracks[i + 1].track_id,
                    'change_pct': round(change_pct * 100, 1),
                    'severity': 'warning'
                })
                
        return issues
    
    def _check_key_compatibility(self, tracks: List[TrackCandidate]) -> List[Dict[str, Any]]:
        """Check harmonic compatibility between tracks."""
        issues = []
        
        for i in range(len(tracks) - 1):
            current_key = tracks[i].key
            next_key = tracks[i + 1].key
            
            # Create temporary candidates to use compatibility method
            dummy_candidate = TrackCandidate(
                track_id='dummy',
                genre='techno',
                bpm=126.0,
                key=next_key,
                energy=0.5,
                duration_bars=64,
                sections=[]
            )
            
            compat_score = dummy_candidate._compute_key_compatibility(current_key)
            
            if compat_score < self.MIN_KEY_COMPATIBILITY:
                issues.append({
                    'type': 'key_conflict',
                    'position': i,
                    'track_a': tracks[i].track_id,
                    'track_b': tracks[i + 1].track_id,
                    'key_a': current_key,
                    'key_b': next_key,
                    'compatibility': round(compat_score, 2),
                    'severity': 'warning'
                })
                
        return issues
    
    def _check_energy_progression(self, tracks: List[TrackCandidate],
                                   energy_curve: EnergyCurve) -> List[Dict[str, Any]]:
        """Check that energy follows the target curve."""
        issues = []
        
        total_duration = sum(t.duration_bars for t in tracks)
        current_bar = 0
        
        for track in tracks:
            track_midpoint = current_bar + track.duration_bars / 2
            target_energy = energy_curve.get_energy_at(track_midpoint * 4)  # Convert to beats
            
            energy_diff = abs(track.energy - target_energy)
            
            if energy_diff > 0.25:
                issues.append({
                    'type': 'energy_deviation',
                    'track': track.track_id,
                    'actual_energy': track.energy,
                    'target_energy': round(target_energy, 2),
                    'deviation': round(energy_diff, 2),
                    'severity': 'warning'
                })
                
            current_bar += track.duration_bars
            
        return issues
    
    def _check_repeats(self, tracks: List[TrackCandidate]) -> List[Dict[str, Any]]:
        """Check for repeated tracks in the set."""
        issues = []
        seen = set()
        
        for i, track in enumerate(tracks):
            if track.track_id in seen:
                issues.append({
                    'type': 'duplicate_track',
                    'position': i,
                    'track_id': track.track_id,
                    'severity': 'error'
                })
            seen.add(track.track_id)
            
        return issues
    
    def _generate_recommendations(self, issues: List[Dict[str, Any]]) -> List[str]:
        """Generate recommendations based on issues found."""
        recommendations = []
        
        for issue in issues:
            if issue['type'] == 'bpm_jump':
                recommendations.append(
                    f"At position {issue['position']}: Consider adding a track with BPM "
                    f"between {issue['bpm_a']} and {issue['bpm_b']}"
                )
            elif issue['type'] == 'key_conflict':
                recommendations.append(
                    f"At position {issue['position']}: Consider transitioning through "
                    f"relative or compatible keys"
                )
            elif issue['type'] == 'duplicate_track':
                recommendations.append(
                    f"Remove duplicate track {issue['track_id']} at position {issue['position']}"
                )
                
        return recommendations


# =============================================================================
# T049: BANGER DETECTION
# =============================================================================

class BangerDetector:
    """
    T049: Detecta y reserva "bangers" (tracks de alta energía > 0.8).
    
    Características de un banger:
    - Energía > 0.8
    - Drop fuerte con build efectivo
    - Elementos de percusión intensos
    - Bass potente
    - Energía espectral alta en rango medio-alto
    """
    
    BANGER_THRESHOLD = 0.8
    BANGER_RESERVE_SIZE = 5  # Keep at least 5 bangers in reserve
    
    def __init__(self):
        self.banger_pool: Set[str] = set()
        self.reserve: Set[str] = set()
        
    def analyze_track(self, track_data: Dict[str, Any]) -> Dict[str, Any]:
        """
        Analyze a track and determine if it's a banger.
        
        Returns:
            Dict with banger analysis
        """
        energy = track_data.get('energy', 0.5)
        sections = track_data.get('sections', [])
        
        # Check for strong drop section
        has_strong_drop = False
        for section in sections:
            if section.get('kind') == 'drop':
                section_energy = section.get('energy', 0.5)
                if section_energy >= 0.9:
                    has_strong_drop = True
                    break
                    
        # Calculate banger score
        banger_score = energy
        if has_strong_drop:
            banger_score += 0.1
            
        # Spectral analysis (if available)
        spectral = track_data.get('spectral_signature', {})
        if spectral:
            # Boost score if high mids present (percussive energy)
            mid_high = spectral.get('mid_high_energy', 0.5)
            if mid_high > 0.7:
                banger_score += 0.05
                
        is_banger = banger_score >= self.BANGER_THRESHOLD
        
        return {
            'is_banger': is_banger,
            'banger_score': round(min(1.0, banger_score), 3),
            'energy': energy,
            'has_strong_drop': has_strong_drop,
            'qualifies_for_reserve': is_banger and has_strong_drop
        }
    
    def add_to_pool(self, track_id: str, track_data: Dict[str, Any]) -> bool:
        """Add track to banger pool if it qualifies."""
        analysis = self.analyze_track(track_data)
        
        if analysis['qualifies_for_reserve']:
            self.banger_pool.add(track_id)
            self._update_reserve()
            return True
            
        return False
    
    def _update_reserve(self) -> None:
        """Maintain banger reserve with top tracks."""
        # In real implementation, sort by banger score and keep top N
        if len(self.banger_pool) > self.BANGER_RESERVE_SIZE:
            # Trim to reserve size
            self.reserve = set(list(self.banger_pool)[:self.BANGER_RESERVE_SIZE])
        else:
            self.reserve = self.banger_pool.copy()
            
    def get_reserve(self) -> List[str]:
        """Get current banger reserve."""
        return list(self.reserve)
    
    def release_from_reserve(self, track_id: str) -> bool:
        """Release a banger from reserve for use in set."""
        if track_id in self.reserve:
            self.reserve.remove(track_id)
            self._update_reserve()
            return True
        return False


# =============================================================================
# T050: WARM-UP SET LOGIC
# =============================================================================

class WarmupSetLogic:
    """
    T050: Lógica especial para sets de warm-up.
    
    - Primera media hora: energía < 0.6
    - BPM gradual: 118 → 126
    - Géneros: deep-house, minimal techno, ambient house
    - No bangers hasta después de 30 minutos
    """
    
    WARMUP_DURATION_MINUTES = 30
    MAX_WARMUP_ENERGY = 0.6
    WARMUP_BPM_RANGE = (118, 126)
    WARMUP_GENRES = ['deep-house', 'minimal', 'ambient', 'dub-techno']
    
    def __init__(self, set_duration_hours: float = 1.5):
        self.set_duration_hours = set_duration_hours
        self.warmup_phase_beats = self.WARMUP_DURATION_MINUTES * 4 * 126  # 30 min @ 126 BPM
        
    def apply_to_template(self, template: SetTemplate) -> SetTemplate:
        """Modify a template for warm-up set."""
        # Adjust energy curve
        template.energy_curve_type = 'ramp_up'
        template.bpm_range = self.WARMUP_BPM_RANGE
        
        return template
    
    def validate_track_for_warmup(self, track: TrackCandidate, 
                                   position_beats: float) -> Dict[str, Any]:
        """
        Validate if a track is appropriate for its position in warm-up set.
        
        Returns:
            Dict with validation results
        """
        in_warmup_phase = position_beats < self.warmup_phase_beats
        
        issues = []
        
        if in_warmup_phase:
            # Check energy
            if track.energy > self.MAX_WARMUP_ENERGY:
                issues.append({
                    'type': 'energy_too_high',
                    'position': position_beats,
                    'track_energy': track.energy,
                    'max_allowed': self.MAX_WARMUP_ENERGY,
                    'message': f"Track energy {track.energy} exceeds warm-up max {self.MAX_WARMUP_ENERGY}"
                })
                
            # Check BPM
            if track.bpm > self.WARMUP_BPM_RANGE[1]:
                issues.append({
                    'type': 'bpm_too_high',
                    'track_bpm': track.bpm,
                    'max_allowed': self.WARMUP_BPM_RANGE[1]
                })
                
            # Check genre
            if track.genre not in self.WARMUP_GENRES:
                issues.append({
                    'type': 'genre_not_warmup',
                    'track_genre': track.genre,
                    'allowed_genres': self.WARMUP_GENRES
                })
                
        return {
            'valid': len(issues) == 0,
            'in_warmup_phase': in_warmup_phase,
            'issues': issues,
            'position_minutes': position_beats / (4 * 126)
        }
    
    def generate_warmup_curve(self) -> EnergyCurve:
        """Generate appropriate energy curve for warm-up set."""
        total_beats = int(self.set_duration_hours * 60 * 4 * 126)
        
        return EnergyCurve(
            curve_type='ramp_up',
            duration_beats=total_beats,
            start_energy=0.25,
            end_energy=0.85,
            peak_energy=0.9,
            peak_position=0.8
        )


# =============================================================================
# T051: REQUEST INJECTION
# =============================================================================

class RequestInjector:
    """
    T051: Sistema para inyectar tracks solicitados por el usuario.
    
    Maneja "must play" tracks encontrando el mejor punto de inserción
    que mantenga la coherencia del set.
    """
    
    def __init__(self):
        self.pending_requests: List[str] = []
        self.injected_requests: List[str] = []
        
    def add_request(self, track_id: str) -> None:
        """Add a user track request."""
        self.pending_requests.append(track_id)
        
    def find_optimal_insertion_point(self, request_track: TrackCandidate,
                                     set_tracks: List[TrackCandidate],
                                     energy_curve: EnergyCurve) -> Dict[str, Any]:
        """
        Find the best position to insert a requested track.
        
        Criteria:
        - Energy alignment with curve at insertion point
        - BPM compatibility with neighbors
        - Key compatibility with neighbors
        - Not during critical energy moments (peak drops)
        """
        best_position = -1
        best_score = -1
        
        for i in range(len(set_tracks) + 1):
            # Calculate score for this position
            score = 0.0
            
            # Get energy at this position
            beats_before = sum(t.duration_bars for t in set_tracks[:i]) * 4
            target_energy = energy_curve.get_energy_at(beats_before)
            
            # Energy match
            energy_diff = abs(request_track.energy - target_energy)
            score += max(0, 1.0 - energy_diff) * 0.3
            
            # BPM compatibility with neighbors
            if i > 0:
                prev_bpm = set_tracks[i - 1].bpm
                bpm_diff = abs(request_track.bpm - prev_bpm) / prev_bpm
                score += max(0, 1.0 - bpm_diff * 10) * 0.25
                
            if i < len(set_tracks):
                next_bpm = set_tracks[i].bpm
                bpm_diff = abs(request_track.bpm - next_bpm) / next_bpm
                score += max(0, 1.0 - bpm_diff * 10) * 0.25
                
            # Key compatibility
            if i > 0:
                prev_key = set_tracks[i - 1].key
                key_score = request_track._compute_key_compatibility(prev_key)
                score += key_score * 0.2
                
            if score > best_score:
                best_score = score
                best_position = i
                
        return {
            'position': best_position,
            'score': round(best_score, 3),
            'reason': f"Best energy/BPM/key alignment at position {best_position}"
        }
    
    def inject_request(self, request_track: TrackCandidate,
                      set_tracks: List[TrackCandidate],
                      energy_curve: EnergyCurve) -> List[TrackCandidate]:
        """
        Inject a requested track into the set at optimal position.
        
        Returns:
            Updated track list with injected request
        """
        insertion = self.find_optimal_insertion_point(request_track, set_tracks, energy_curve)
        position = insertion['position']
        
        # Insert at calculated position
        new_list = set_tracks[:position] + [request_track] + set_tracks[position:]
        
        self.pending_requests.remove(request_track.track_id)
        self.injected_requests.append(request_track.track_id)
        
        logger.info(f"[REQUEST_INJECT] Injected {request_track.track_id} at position {position}")
        
        return new_list


# =============================================================================
# T052: MEMORY/HISTORY CHECK
# =============================================================================

class PlayHistoryTracker:
    """
    T052: Sistema de memoria para evitar repeticiones.
    
    Mantiene historial de tracks reproducidos con:
    - Timestamps de última reproducción
    - Conteo de reproducciones totales
    - Penalización temporal (decay con el tiempo)
    """
    
    RECENT_WINDOW_DAYS = 7  # Tracks played in last 7 days are "recent"
    FATIGUE_DECAY_DAYS = 30  # Full reset after 30 days
    MAX_FATIGUE_PENALTY = 0.5  # Maximum penalty to score
    
    def __init__(self, history_file: Optional[str] = None):
        self.history_file = history_file
        self.play_history: Dict[str, List[datetime]] = defaultdict(list)
        self._load_history()
        
    def _load_history(self) -> None:
        """Load play history from file if available."""
        if self.history_file and os.path.exists(self.history_file):
            try:
                with open(self.history_file, 'r') as f:
                    data = json.load(f)
                    for track_id, timestamps in data.items():
                        self.play_history[track_id] = [
                            datetime.fromisoformat(ts) for ts in timestamps
                        ]
            except Exception as e:
                logger.warning(f"Failed to load play history: {e}")
                
    def record_play(self, track_id: str) -> None:
        """Record that a track was played."""
        self.play_history[track_id].append(datetime.now())
        self._save_history()
        
    def get_fatigue_score(self, track_id: str) -> float:
        """
        Calculate fatigue score for a track (0.0 = fresh, 1.0 = overplayed).
        
        Based on:
        - Time since last play
        - Total play count
        - Recent play frequency
        """
        if track_id not in self.play_history:
            return 0.0
            
        plays = self.play_history[track_id]
        now = datetime.now()
        
        # Calculate recency factor
        last_play = plays[-1]
        days_since = (now - last_play).days
        recency_factor = max(0, 1 - (days_since / self.FATIGUE_DECAY_DAYS))
        
        # Calculate frequency factor
        recent_plays = [p for p in plays if (now - p).days <= self.RECENT_WINDOW_DAYS]
        frequency_factor = min(1.0, len(recent_plays) / 3)  # 3+ plays in 7 days = max fatigue
        
        # Total plays factor (diminishing returns)
        total_factor = min(1.0, len(plays) / 10)  # 10+ total plays = max fatigue
        
        # Combined fatigue
        fatigue = (recency_factor * 0.4 + frequency_factor * 0.4 + total_factor * 0.2)
        
        return round(fatigue, 3)
    
    def get_penalty_for_selection(self, track_id: str) -> float:
        """Get selection penalty based on fatigue (0.0 = no penalty)."""
        fatigue = self.get_fatigue_score(track_id)
        return fatigue * self.MAX_FATIGUE_PENALTY
    
    def is_track_available(self, track_id: str, min_days_since: int = 1) -> bool:
        """Check if track is available (not played too recently)."""
        if track_id not in self.play_history:
            return True
            
        last_play = self.play_history[track_id][-1]
        days_since = (datetime.now() - last_play).days
        
        return days_since >= min_days_since
    
    def get_recent_tracks(self, days: int = 7) -> Set[str]:
        """Get set of tracks played in the last N days."""
        cutoff = datetime.now() - timedelta(days=days)
        recent = set()
        
        for track_id, plays in self.play_history.items():
            if any(p > cutoff for p in plays):
                recent.add(track_id)
                
        return recent
    
    def _save_history(self) -> None:
        """Save play history to file."""
        if self.history_file:
            try:
                data = {
                    track_id: [p.isoformat() for p in plays]
                    for track_id, plays in self.play_history.items()
                }
                with open(self.history_file, 'w') as f:
                    json.dump(data, f, indent=2)
            except Exception as e:
                logger.warning(f"Failed to save play history: {e}")
                
    def reset_history(self) -> None:
        """Clear all play history."""
        self.play_history.clear()
        if self.history_file and os.path.exists(self.history_file):
            os.remove(self.history_file)


# =============================================================================
# T053: GENRE-FLUID TRANSITIONS
# =============================================================================

class GenreFluidTransition:
    """
    T053: Sistema para transiciones fluidas entre géneros.
    
    Soporta transiciones como:
    - 125 BPM tech-house → 140 BPM techno
    - House → Techno
    - Trance → Progressive
    - Reggaeton → Latin House
    """
    
    # Mapa de transiciones válidas con puentes intermedios
    GENRE_TRANSITION_MAP = {
        ('house', 'techno'): {
            'bridge_genre': 'tech-house',
            'bpm_strategy': 'ramp',
            'energy_boost': 0.1
        },
        ('tech-house', 'techno'): {
            'bridge_genre': 'peak-time-techno',
            'bpm_strategy': 'ramp',
            'energy_boost': 0.15
        },
        ('deep-house', 'techno'): {
            'bridge_genre': 'tech-house',
            'bpm_strategy': 'step',
            'energy_boost': 0.2
        },
        ('trance', 'techno'): {
            'bridge_genre': 'progressive-techno',
            'bpm_strategy': 'maintain',
            'energy_boost': 0.1
        },
        ('reggaeton', 'latin-house'): {
            'bridge_genre': 'latin-tech-house',
            'bpm_strategy': 'double-time',
            'energy_boost': 0.05
        },
        ('drum-and-bass', 'techno'): {
            'bridge_genre': 'halftime-dnb',
            'bpm_strategy': 'half-time',
            'energy_boost': 0.0
        },
    }
    
    BPM_RANGES = {
        'house': (120, 128),
        'deep-house': (118, 124),
        'tech-house': (122, 128),
        'techno': (125, 140),
        'trance': (135, 150),
        'drum-and-bass': (160, 180),
        'reggaeton': (88, 98),
        'latin-house': (122, 128),
    }
    
    def plan_transition(self, from_genre: str, to_genre: str,
                       current_bpm: float, target_bpm: float) -> Dict[str, Any]:
        """
        Plan a genre-fluid transition.
        
        Returns:
            Dict with transition plan including bridge tracks and BPM strategy
        """
        key = (from_genre, to_genre)
        reverse_key = (to_genre, from_genre)
        
        config = self.GENRE_TRANSITION_MAP.get(key) or self.GENRE_TRANSITION_MAP.get(reverse_key)
        
        if not config:
            # No predefined path - use direct transition with gradual BPM change
            return self._plan_direct_transition(from_genre, to_genre, current_bpm, target_bpm)
            
        bridge_genre = config['bridge_genre']
        bpm_strategy = config['bpm_strategy']
        
        # Calculate BPM path
        bpm_path = self._calculate_bpm_path(
            current_bpm, target_bpm, bpm_strategy, bridge_genre
        )
        
        return {
            'from_genre': from_genre,
            'to_genre': to_genre,
            'bridge_genre': bridge_genre,
            'bpm_strategy': bpm_strategy,
            'bpm_path': bpm_path,
            'num_bridge_tracks': len(bpm_path) - 2,
            'energy_boost': config['energy_boost'],
            'recommended_transition_point': 'bridge_drop' if bpm_strategy == 'ramp' else 'breakdown'
        }
    
    def _plan_direct_transition(self, from_genre: str, to_genre: str,
                                current_bpm: float, target_bpm: float) -> Dict[str, Any]:
        """Plan direct transition without bridge genre."""
        bpm_diff = target_bpm - current_bpm
        
        if abs(bpm_diff) <= 10:
            # Small difference - single transition track
            bpm_path = [current_bpm, target_bpm]
            num_bridges = 0
        else:
            # Large difference - need intermediate BPM
            mid_bpm = (current_bpm + target_bpm) / 2
            bpm_path = [current_bpm, mid_bpm, target_bpm]
            num_bridges = 1
            
        return {
            'from_genre': from_genre,
            'to_genre': to_genre,
            'bridge_genre': None,
            'bpm_strategy': 'direct',
            'bpm_path': bpm_path,
            'num_bridge_tracks': num_bridges,
            'energy_boost': 0.1 if abs(bpm_diff) > 15 else 0.05,
            'recommended_transition_point': 'breakdown'
        }
    
    def _calculate_bpm_path(self, start_bpm: float, end_bpm: float,
                           strategy: str, bridge_genre: str) -> List[float]:
        """Calculate BPM progression path."""
        if strategy == 'ramp':
            # Gradual ramp through bridge genre BPM range
            bridge_range = self.BPM_RANGES.get(bridge_genre, (120, 128))
            return [start_bpm, bridge_range[0], bridge_range[1], end_bpm]
            
        elif strategy == 'step':
            # Discrete steps
            mid = (start_bpm + end_bpm) / 2
            return [start_bpm, mid, end_bpm]
            
        elif strategy == 'maintain':
            # Keep BPM similar, change genre only
            avg = (start_bpm + end_bpm) / 2
            return [avg, avg]
            
        elif strategy == 'double-time':
            # Double the BPM (e.g., 95 → 190, then half to 130)
            return [start_bpm, start_bpm * 2, end_bpm]
            
        elif strategy == 'half-time':
            # Half the BPM
            return [start_bpm, start_bpm / 2, end_bpm]
            
        else:
            return [start_bpm, end_bpm]
    
    def validate_bpm_transition(self, from_bpm: float, to_bpm: float) -> Dict[str, Any]:
        """Validate if BPM transition is acceptable."""
        change_pct = abs(to_bpm - from_bpm) / from_bpm
        
        is_valid = change_pct <= 0.06  # 6% max
        
        return {
            'valid': is_valid,
            'change_pct': round(change_pct * 100, 1),
            'from_bpm': from_bpm,
            'to_bpm': to_bpm,
            'needs_bridge': change_pct > 0.06,
            'recommended_approach': 'bridge' if change_pct > 0.06 else 'direct'
        }


# =============================================================================
# T054: DRUM FILL INJECTION
# =============================================================================

class DrumFillInjector:
    """
    T054: Sistema para inyectar fills de batería personalizados.
    
    Tipos de fills:
    - snare_roll: Redoble de snare para builds
    - tom_fill: Toms para transiciones
    - kick_fill: Pattern de kicks para energía
    - crash_fill: Crashes para acentuar drops
    """
    
    FILL_TYPES = {
        'snare_roll': {
            'midi_note': 38,  # D1 - Snare
            'pattern': 'accelerating',
            'velocity_curve': 'exponential_rise',
            'typical_bars': 1
        },
        'tom_fill': {
            'midi_notes': [41, 43, 45, 47],  # Toms
            'pattern': 'descending',
            'velocity_curve': 'linear',
            'typical_bars': 1
        },
        'kick_fill': {
            'midi_note': 36,  # C1 - Kick
            'pattern': 'burst',
            'velocity_curve': 'random',
            'typical_bars': 0.5
        },
        'crash_fill': {
            'midi_notes': [49, 57],  # Crashes
            'pattern': 'hit',
            'velocity_curve': 'peak',
            'typical_bars': 0.25
        }
    }
    
    def generate_fill(self, fill_type: str, start_bar: float, 
                     intensity: str = 'medium') -> Dict[str, Any]:
        """
        Generate MIDI notes for a drum fill.
        
        Args:
            fill_type: Type of fill to generate
            start_bar: Start position in bars
            intensity: 'light', 'medium', 'heavy'
            
        Returns:
            Dict with MIDI note data
        """
        config = self.FILL_TYPES.get(fill_type)
        if not config:
            return {'error': f'Unknown fill type: {fill_type}'}
            
        notes = []
        
        if fill_type == 'snare_roll':
            notes = self._generate_snare_roll(start_bar, intensity)
        elif fill_type == 'tom_fill':
            notes = self._generate_tom_fill(start_bar, intensity)
        elif fill_type == 'kick_fill':
            notes = self._generate_kick_fill(start_bar, intensity)
        elif fill_type == 'crash_fill':
            notes = self._generate_crash_fill(start_bar, intensity)
            
        return {
            'fill_type': fill_type,
            'start_bar': start_bar,
            'intensity': intensity,
            'notes': notes,
            'track': 'DRUMS',
            'recommended_placement': self._get_recommended_placement(fill_type)
        }
    
    def _generate_snare_roll(self, start_bar: float, intensity: str) -> List[Dict]:
        """Generate snare roll MIDI notes."""
        notes = []
        
        subdivisions = {'light': 8, 'medium': 16, 'heavy': 32}[intensity]
        start_velocity = {'light': 60, 'medium': 70, 'heavy': 80}[intensity]
        end_velocity = {'light': 100, 'medium': 115, 'heavy': 127}[intensity]
        
        bar_duration = 1.0  # 1 bar roll
        step = bar_duration / subdivisions
        
        for i in range(subdivisions):
            t = start_bar + i * step
            # Exponential velocity curve
            progress = i / subdivisions
            velocity = int(start_velocity + (end_velocity - start_velocity) * (progress ** 1.5))
            
            notes.append({
                'pitch': 38,  # Snare
                'start_time': t * 4,  # Convert to beats
                'duration': 0.1,
                'velocity': velocity,
                'mute': False
            })
            
        return notes
    
    def _generate_tom_fill(self, start_bar: float, intensity: str) -> List[Dict]:
        """Generate tom fill MIDI notes."""
        notes = []
        toms = [41, 43, 45, 47, 48, 50]  # Low to high
        
        num_hits = {'light': 4, 'medium': 6, 'heavy': 8}[intensity]
        velocity = {'light': 80, 'medium': 100, 'heavy': 120}[intensity]
        
        for i in range(num_hits):
            t = start_bar + i * (1.0 / num_hits)
            pitch = toms[i % len(toms)]
            
            notes.append({
                'pitch': pitch,
                'start_time': t * 4,
                'duration': 0.2,
                'velocity': velocity,
                'mute': False
            })
            
        return notes
    
    def _generate_kick_fill(self, start_bar: float, intensity: str) -> List[Dict]:
        """Generate kick burst fill."""
        notes = []
        
        num_kicks = {'light': 4, 'medium': 6, 'heavy': 8}[intensity]
        base_velocity = {'light': 100, 'medium': 110, 'heavy': 127}[intensity]
        
        for i in range(num_kicks):
            t = start_bar + i * (0.5 / num_kicks)
            velocity = base_velocity - i * 5  # Decaying
            
            notes.append({
                'pitch': 36,  # Kick
                'start_time': t * 4,
                'duration': 0.25,
                'velocity': max(80, velocity),
                'mute': False
            })
            
        return notes
    
    def _generate_crash_fill(self, start_bar: float, intensity: str) -> List[Dict]:
        """Generate crash hit fill."""
        notes = []
        
        crashes = [49, 57]  # Crash 1, Crash 2
        velocity = {'light': 90, 'medium': 110, 'heavy': 127}[intensity]
        
        notes.append({
            'pitch': crashes[0],
            'start_time': start_bar * 4,
            'duration': 2.0,
            'velocity': velocity,
            'mute': False
        })
        
        if intensity == 'heavy':
            notes.append({
                'pitch': crashes[1],
                'start_time': start_bar * 4 + 0.25,
                'duration': 2.0,
                'velocity': int(velocity * 0.9),
                'mute': False
            })
            
        return notes
    
    def _get_recommended_placement(self, fill_type: str) -> str:
        """Get recommended placement for fill type."""
        placements = {
            'snare_roll': 'build_section_last_bar',
            'tom_fill': 'transition_before_chorus',
            'kick_fill': 'drop_entry',
            'crash_fill': 'drop_downbeat'
        }
        return placements.get(fill_type, 'section_transition')
    
    def inject_fills_for_track(self, track_sections: List[SectionTag]) -> List[Dict[str, Any]]:
        """
        Automatically inject appropriate fills throughout a track.
        
        Returns:
            List of fill configurations
        """
        fills = []
        
        for section in track_sections:
            if section.kind == 'build':
                # Add snare roll at end of build
                fills.append(self.generate_fill(
                    'snare_roll', section.end_bar - 1, 'medium'
                ))
                
            elif section.kind == 'drop':
                # Add crash at drop start
                fills.append(self.generate_fill(
                    'crash_fill', section.start_bar, 'heavy'
                ))
                
                # Add kick fill at drop entry
                fills.append(self.generate_fill(
                    'kick_fill', section.start_bar + 0.5, 'medium'
                ))
                
            elif section.kind == 'break':
                # Add tom fill before break ends
                fills.append(self.generate_fill(
                    'tom_fill', section.end_bar - 0.5, 'light'
                ))
                
        return fills


# =============================================================================
# T055: CROWD NOISE OVERLAY
# =============================================================================

class CrowdNoiseOverlay:
    """
    T055: Sistema para overlay de ruido de multitud.
    
    Coloca automáticamente:
    - Cheers/risas en drops
    - Ambiente de club entre tracks
    - Reactividad proporcional a energía
    """
    
    CROWD_SAMPLES = {
        'cheer_small': {'energy': 0.3, 'duration': 2.0},
        'cheer_medium': {'energy': 0.5, 'duration': 3.0},
        'cheer_big': {'energy': 0.8, 'duration': 4.0},
        'crowd_ambience': {'energy': 0.2, 'duration': 8.0},
        'claps': {'energy': 0.4, 'duration': 2.0},
        'whistle': {'energy': 0.6, 'duration': 1.5},
        'shout': {'energy': 0.7, 'duration': 1.0},
    }
    
    def __init__(self):
        self.overlay_events: List[Dict[str, Any]] = []
        
    def generate_overlays_for_set(self, tracks: List[TrackCandidate],
                                  sections_per_track: List[List[SectionTag]]) -> List[Dict[str, Any]]:
        """
        Generate crowd noise overlays for an entire set.
        
        Returns:
            List of overlay events
        """
        overlays = []
        current_bar = 0.0
        
        for i, (track, sections) in enumerate(zip(tracks, sections_per_track)):
            track_overlays = self._generate_for_track(track, sections, current_bar)
            overlays.extend(track_overlays)
            current_bar += track.duration_bars
            
        return overlays
    
    def _generate_for_track(self, track: TrackCandidate, 
                           sections: List[SectionTag],
                           track_start_bar: float) -> List[Dict[str, Any]]:
        """Generate overlays for a single track."""
        overlays = []
        
        for section in sections:
            section_bar = track_start_bar + section.start_bar
            
            if section.kind == 'drop' and section.energy >= 0.8:
                # Big cheer on high-energy drop
                overlays.append({
                    'type': 'crowd_noise',
                    'sample': 'cheer_big',
                    'position_bar': section_bar,
                    'volume': 0.4,
                    'trigger': 'drop_energy_high'
                })
                
            elif section.kind == 'drop':
                # Medium cheer on regular drop
                overlays.append({
                    'type': 'crowd_noise',
                    'sample': 'cheer_medium',
                    'position_bar': section_bar,
                    'volume': 0.3,
                    'trigger': 'drop_entry'
                })
                
            elif section.kind == 'build' and section.energy >= 0.75:
                # Claps during intense build
                overlays.append({
                    'type': 'crowd_noise',
                    'sample': 'claps',
                    'position_bar': section_bar + (section.end_bar - section.start_bar) / 2,
                    'volume': 0.25,
                    'trigger': 'build_midpoint'
                })
                
        return overlays
    
    def get_sample_for_event(self, event_type: str, energy: float) -> str:
        """Select appropriate crowd sample based on event type and energy."""
        if event_type == 'drop':
            if energy >= 0.9:
                return 'cheer_big'
            elif energy >= 0.7:
                return 'cheer_medium'
            else:
                return 'cheer_small'
        elif event_type == 'build':
            return 'claps'
        elif event_type == 'transition':
            return 'crowd_ambience'
        else:
            return 'cheer_small'


# =============================================================================
# T056: CONTINUOUS ARRANGEMENT
# =============================================================================

class ContinuousArrangement:
    """
    T056: Sistema para crear arrangement continuo uniendo generaciones.
    
    Une múltiples tracks en un set continuo:
    - Calcula puntos de transición óptimos
    - Ajusta BPM y key gradualmente
    - Crea crossfades y overlaps
    - Genera automation para transiciones suaves
    """
    
    def __init__(self):
        self.transition_events: List[Dict[str, Any]] = []
        
    def stitch_tracks(self, tracks: List[TrackCandidate],
                     mixing_mode: MixingMode = None) -> Dict[str, Any]:
        """
        Stitch multiple tracks into continuous arrangement.
        
        Returns:
            Dict with complete arrangement plan
        """
        if mixing_mode is None:
            mixing_mode = FastMixingMode()
            
        timeline = []
        current_bar = 0.0
        
        for i in range(len(tracks) - 1):
            track_a = tracks[i]
            track_b = tracks[i + 1]
            
            # Convert section dicts to SectionTag objects
            track_a_section_tags = self._convert_sections_to_tags(track_a.sections)
            track_b_section_tags = self._convert_sections_to_tags(track_b.sections)
            
            # Calculate transition
            transition = mixing_mode.calculate_transition_points(
                track_a.duration_bars,
                track_b.duration_bars,
                track_a_section_tags,
                track_b_section_tags
            )
            
            # Add track A to timeline
            timeline.append({
                'track_id': track_a.track_id,
                'start_bar': current_bar,
                'end_bar': current_bar + transition['track_a_out_bar'],
                'type': 'full_track',
                'sections': self._remap_sections(track_a.sections, current_bar)
            })
            
            # Add transition overlap
            overlap_start = current_bar + transition['transition_start_bar']
            overlap_end = current_bar + transition['track_a_out_bar']
            
            timeline.append({
                'track_id': f"transition_{i}_{i+1}",
                'start_bar': overlap_start,
                'end_bar': overlap_end,
                'type': 'transition',
                'tracks_mixing': [track_a.track_id, track_b.track_id],
                'automation': self._generate_transition_automation(
                    overlap_start, overlap_end
                )
            })
            
            current_bar += track_a.duration_bars - transition['overlap_bars']
            
        # Add final track
        if tracks:
            final_track = tracks[-1]
            timeline.append({
                'track_id': final_track.track_id,
                'start_bar': current_bar,
                'end_bar': current_bar + final_track.duration_bars,
                'type': 'full_track',
                'sections': self._remap_sections(final_track.sections, current_bar)
            })
            
        return {
            'timeline': timeline,
            'total_bars': current_bar + (tracks[-1].duration_bars if tracks else 0),
            'transitions': len(tracks) - 1 if len(tracks) > 1 else 0,
            'mixing_mode': mixing_mode.config.get('description', 'standard')
        }
    
    def _convert_sections_to_tags(self, sections: List[Dict[str, Any]]) -> List[SectionTag]:
        """Convert section dictionaries to SectionTag objects."""
        tags = []
        for section in sections:
            if isinstance(section, SectionTag):
                tags.append(section)
            elif isinstance(section, dict):
                tags.append(SectionTag(
                    kind=section.get('kind', 'verse'),
                    start_bar=float(section.get('start_bar', 0)),
                    end_bar=float(section.get('end_bar', 16)),
                    energy=float(section.get('energy', 0.5)),
                    confidence=float(section.get('confidence', 0.8)),
                    tags=section.get('tags', [])
                ))
        return tags
    
    def _remap_sections(self, sections: List[Any], offset_bar: float) -> List[Dict]:
        """Remap section positions with offset."""
        remapped = []
        for section in sections:
            if isinstance(section, SectionTag):
                remapped.append({
                    'kind': section.kind,
                    'start_bar': section.start_bar + offset_bar,
                    'end_bar': section.end_bar + offset_bar,
                    'energy': section.energy
                })
            elif isinstance(section, dict):
                remapped.append({
                    'kind': section.get('kind', 'verse'),
                    'start_bar': section.get('start_bar', 0) + offset_bar,
                    'end_bar': section.get('end_bar', 16) + offset_bar,
                    'energy': section.get('energy', 0.5)
                })
        return remapped
    
    def _generate_transition_automation(self, start_bar: float, 
                                         end_bar: float) -> Dict[str, Any]:
        """Generate volume/filter automation for transition."""
        duration = end_bar - start_bar
        
        return {
            'track_a_fade': {
                'start_bar': start_bar,
                'end_bar': end_bar,
                'start_volume': 0.85,
                'end_volume': 0.0,
                'curve': 'exponential'
            },
            'track_b_fade': {
                'start_bar': start_bar,
                'end_bar': end_bar,
                'start_volume': 0.0,
                'end_volume': 0.85,
                'curve': 'exponential'
            },
            'filter_sweep': {
                'track_a': {'start_freq': 20000, 'end_freq': 800, 'resonance': 0.3},
                'track_b': {'start_freq': 800, 'end_freq': 20000, 'resonance': 0.3}
            }
        }


# =============================================================================
# T057: TRANSITION TYPE RANDOMIZER
# =============================================================================

class TransitionTypeRandomizer:
    """
    T057: Randomizador probabilístico de tipos de transición.
    
    Selecciona técnicas de transición basado en:
    - Energía de las tracks
    - Género
    - Posición en el set
    - Probabilidades configurables
    """
    
    TRANSITION_TYPES = {
        'filter_sweep': {'weight': 0.25, 'energy_range': (0.3, 1.0)},
        'beatmatch_cut': {'weight': 0.20, 'energy_range': (0.7, 1.0)},
        'echo_out': {'weight': 0.15, 'energy_range': (0.2, 0.8)},
        'reverb_tail': {'weight': 0.15, 'energy_range': (0.2, 0.6)},
        'loop_roll': {'weight': 0.10, 'energy_range': (0.5, 1.0)},
        'acapella_in': {'weight': 0.08, 'energy_range': (0.4, 0.9)},
        'drop_swap': {'weight': 0.07, 'energy_range': (0.8, 1.0)},
    }
    
    def __init__(self, seed: int = None):
        self.rng = random.Random(seed)
        
    def select_transition_type(self, track_a_energy: float, track_b_energy: float,
                              position_in_set: float) -> Dict[str, Any]:
        """
        Select transition type based on context.
        
        Args:
            track_a_energy: Energy of outgoing track
            track_b_energy: Energy of incoming track
            position_in_set: 0.0-1.0 position in set
            
        Returns:
            Selected transition type with parameters
        """
        avg_energy = (track_a_energy + track_b_energy) / 2
        
        # Filter by energy range
        valid_types = {}
        for t_type, config in self.TRANSITION_TYPES.items():
            min_e, max_e = config['energy_range']
            if min_e <= avg_energy <= max_e:
                # Adjust weight by position in set
                weight = config['weight']
                
                # Boost certain types for specific positions
                if position_in_set < 0.2 and t_type == 'echo_out':
                    weight *= 1.5  # More echo outs at start
                elif position_in_set > 0.8 and t_type == 'drop_swap':
                    weight *= 1.3  # More drop swaps at peak
                    
                valid_types[t_type] = weight
                
        if not valid_types:
            return {'type': 'filter_sweep', 'confidence': 0.5}
            
        # Weighted random selection
        total = sum(valid_types.values())
        pick = self.rng.uniform(0, total)
        
        current = 0
        for t_type, weight in valid_types.items():
            current += weight
            if pick <= current:
                return {
                    'type': t_type,
                    'confidence': weight / total,
                    'parameters': self._get_transition_params(t_type)
                }
                
        return {'type': 'filter_sweep', 'confidence': 0.5}
    
    def _get_transition_params(self, t_type: str) -> Dict[str, Any]:
        """Get default parameters for transition type."""
        params = {
            'filter_sweep': {'duration_bars': 4, 'cutoff_start': 20000, 'cutoff_end': 400},
            'beatmatch_cut': {'sync_bars': 2, 'cut_point': 'downbeat'},
            'echo_out': {'feedback': 0.6, 'decay_time': 1.5},
            'reverb_tail': {'decay': 2.0, 'wet_dry': 0.8},
            'loop_roll': {'roll_size': 0.25, 'duration_bars': 2},
            'acapella_in': {'fade_in_bars': 2, 'volume': 0.7},
            'drop_swap': {'timing': 'on_drop', 'impact': 'high'},
        }
        return params.get(t_type, {})


# =============================================================================
# T058: DROP SWAP
# =============================================================================

class DropSwap:
    """
    T058: Sistema para "Drop Swap" - usar el drop de track B después del build de track A.
    
    Técnica avanzada de DJ:
    - Reproduce build de track A
    - En el punto de drop, cambia al drop de track B
    - Crea efecto de "sorpresa" y energía máxima
    """
    
    def __init__(self):
        self.swap_points: List[Dict[str, Any]] = []
        
    def plan_drop_swap(self, track_a: TrackCandidate, track_b: TrackCandidate,
                      current_bar: float) -> Optional[Dict[str, Any]]:
        """
        Plan a drop swap between two tracks.
        
        Returns:
            Swap configuration or None if not feasible
        """
        # Find build in track A
        build_a = None
        for section in track_a.sections:
            section_kind = section.kind if isinstance(section, SectionTag) else section.get('kind', '')
            if section_kind == 'build':
                build_a = section
                break
                
        # Find drop in track B
        drop_b = None
        for section in track_b.sections:
            section_kind = section.kind if isinstance(section, SectionTag) else section.get('kind', '')
            if section_kind == 'drop':
                drop_b = section
                break
                
        if not build_a or not drop_b:
            return None
            
        # Get section values handling both SectionTag and dict
        build_a_start = build_a.start_bar if isinstance(build_a, SectionTag) else build_a.get('start_bar', 0)
        build_a_end = build_a.end_bar if isinstance(build_a, SectionTag) else build_a.get('end_bar', 16)
        drop_b_start = drop_b.start_bar if isinstance(drop_b, SectionTag) else drop_b.get('start_bar', 0)
        
        # Calculate swap point
        swap_bar = current_bar + build_a_end
        
        return {
            'type': 'drop_swap',
            'enabled': True,
            'track_a_id': track_a.track_id,
            'track_b_id': track_b.track_id,
            'swap_point_bar': swap_bar,
            'swap_point_beat': swap_bar * 4,
            'preparation': {
                'cue_track_b_at': build_a_start,  # Start track B at its intro
                'sync_point': 'build_end_beat'
            },
            'automation': {
                'track_a_cut': {'bar': swap_bar, 'curve': 'immediate'},
                'track_b_start': {'bar': swap_bar, 'offset_in_track_b': drop_b_start},
                'impact_fx': {'bar': swap_bar, 'type': 'crash'}
            },
            'energy_impact': track_b.energy * 1.1,  # Boost from surprise effect
            'risk_level': 'medium'  # Technically demanding
        }
    
    def validate_drop_swap(self, swap_plan: Dict[str, Any]) -> Dict[str, Any]:
        """Validate if a drop swap will work smoothly."""
        issues = []
        
        # Check BPM compatibility
        # (Would need actual track BPMs from plan)
        
        # Check key compatibility
        # (Would need actual track keys from plan)
        
        # Check energy differential
        # Big energy jumps might be too jarring
        
        is_valid = len(issues) == 0
        
        return {
            'valid': is_valid,
            'issues': issues,
            'recommendation': 'Execute' if is_valid else 'Use standard transition'
        }


# =============================================================================
# T059: BPM ANCHOR POINTS
# =============================================================================

class BPMAnchorPoints:
    """
    T059: Sistema de puntos de anclaje BPM dinámicos.
    
    Permite cambios de BPM planificados en puntos específicos del set.
    Útil para:
    - Ramp up gradual de BPM
    - Cambios de género que requieren BPM diferente
    - Energización mediante aceleración
    """
    
    def __init__(self, base_bpm: float = 126.0):
        self.base_bpm = base_bpm
        self.anchors: List[Dict[str, Any]] = []
        
    def add_anchor(self, bar: float, target_bpm: float, 
                  transition_bars: int = 8) -> Dict[str, Any]:
        """
        Add a BPM anchor point.
        
        Args:
            bar: Position in bars
            target_bpm: Target BPM at this point
            transition_bars: Bars to smoothly transition to this BPM
            
        Returns:
            Anchor configuration
        """
        anchor = {
            'bar': bar,
            'beat': bar * 4,
            'target_bpm': target_bpm,
            'transition_bars': transition_bars,
            'transition_start_bar': bar - transition_bars,
            'type': 'anchor_point'
        }
        
        self.anchors.append(anchor)
        return anchor
    
    def generate_bpm_curve(self, total_bars: int) -> List[Dict[str, Any]]:
        """
        Generate complete BPM curve with all anchors.
        
        Returns:
            List of BPM points over time
        """
        if not self.anchors:
            return [{'bar': 0, 'bpm': self.base_bpm}]
            
        # Sort anchors by position
        sorted_anchors = sorted(self.anchors, key=lambda a: a['bar'])
        
        curve = []
        current_bpm = self.base_bpm
        
        # Start point
        curve.append({'bar': 0, 'bpm': current_bpm})
        
        for anchor in sorted_anchors:
            # Add transition start
            curve.append({
                'bar': anchor['transition_start_bar'],
                'bpm': current_bpm,
                'note': 'transition_start'
            })
            
            # Add anchor point (target BPM)
            curve.append({
                'bar': anchor['bar'],
                'bpm': anchor['target_bpm'],
                'note': 'anchor'
            })
            
            current_bpm = anchor['target_bpm']
            
        # End point
        curve.append({'bar': total_bars, 'bpm': current_bpm})
        
        return curve
    
    def calculate_tempo_automation(self) -> List[Dict[str, Any]]:
        """Calculate tempo automation points for Ableton."""
        points = []
        
        for i, anchor in enumerate(self.anchors):
            # Ramp from previous BPM to this anchor
            points.append({
                'time': anchor['transition_start_bar'] * 4,  # In beats
                'tempo': anchor.get('start_bpm', self.base_bpm if i == 0 else self.anchors[i-1]['target_bpm']),
                'curve': 'linear'
            })
            
            points.append({
                'time': anchor['bar'] * 4,
                'tempo': anchor['target_bpm'],
                'curve': 'linear'
            })
            
        return points
    
    def plan_ramp(self, start_bpm: float, end_bpm: float, 
                 duration_bars: int, curve_type: str = 'linear') -> Dict[str, Any]:
        """
        Plan a BPM ramp over a duration.
        
        Args:
            start_bpm: Starting BPM
            end_bpm: Ending BPM
            duration_bars: Duration of ramp in bars
            curve_type: 'linear', 'exponential', 'ease_in_out'
            
        Returns:
            Ramp configuration
        """
        self.base_bpm = start_bpm
        
        # Clear existing anchors and create single ramp
        self.anchors = []
        
        self.add_anchor(
            bar=duration_bars,
            target_bpm=end_bpm,
            transition_bars=duration_bars
        )
        
        return {
            'start_bpm': start_bpm,
            'end_bpm': end_bpm,
            'duration_bars': duration_bars,
            'curve_type': curve_type,
            'anchors': self.anchors
        }


# =============================================================================
# MAIN SET GENERATOR CLASS
# =============================================================================

class SetGenerator:
    """
    Generador principal de sets DJ.
    
    Integra todas las características T041-T060 para crear sets completos.
    """
    
    def __init__(self, library_path: Optional[str] = None):
        self.library = TrackLibrary(library_path)
        self.coherence_engine = SetCoherenceEngine()
        self.banger_detector = BangerDetector()
        self.history_tracker = PlayHistoryTracker()
        self.section_tagger = SectionTaggingEngine()
        self.hot_cue_generator = HotCueGenerator()
        self.genre_transition = GenreFluidTransition()
        self.fill_injector = DrumFillInjector()
        self.crowd_overlay = CrowdNoiseOverlay()
        self.continuous_arrangement = ContinuousArrangement()
        self.transition_randomizer = TransitionTypeRandomizer()
        self.drop_swap = DropSwap()
        self.bpm_anchors = BPMAnchorPoints()
        
        self.logger = logging.getLogger("SetGenerator")
        
    def generate_set(self, 
                    template_name: str = '2hr_standard',
                    genre: str = 'tech-house',
                    start_bpm: float = 126.0,
                    energy_curve_type: str = 'mountain',
                    user_requests: Optional[List[str]] = None,
                    mixing_mode: str = 'standard') -> Dict[str, Any]:
        """
        Generate a complete DJ set.
        
        Args:
            template_name: Template to use (1hr_peak_time, 2hr_standard, etc.)
            genre: Starting genre
            start_bpm: Starting BPM
            energy_curve_type: Type of energy curve
            user_requests: List of track IDs to include
            mixing_mode: 'fast', 'long_blend', 'standard'
            
        Returns:
            Complete set plan
        """
        # Get template
        template = SET_TEMPLATES.get(template_name)
        if not template:
            return {'error': f'Unknown template: {template_name}'}
            
        # Create energy curve
        energy_curve = EnergyCurve(
            curve_type=energy_curve_type or template.energy_curve_type,
            duration_beats=template.duration_beats,
            start_energy=0.3,
            end_energy=0.3,
            peak_energy=1.0,
            peak_position=0.5
        )
        
        # Select tracks
        selected_tracks = self.library.select_tracks_for_set(
            template, energy_curve, genre, user_requests
        )
        
        # Validate coherence
        coherence_result = self.coherence_engine.validate_set(
            selected_tracks, energy_curve
        )
        
        # Tag sections for each track
        tagged_sections = []
        for track in selected_tracks:
            sections = self.section_tagger.tag_sections({
                'duration_bars': track.duration_bars,
                'sections': track.sections
            })
            tagged_sections.append(sections)
            
        # Generate hot cues
        hot_cues = []
        for track, sections in zip(selected_tracks, tagged_sections):
            cues = self.hot_cue_generator.generate_hot_cues(
                sections, track.genre, track.bpm
            )
            hot_cues.append({
                'track_id': track.track_id,
                'cues': [c.to_dict() for c in cues]
            })
            
        # Create continuous arrangement
        mixing = FastMixingMode() if mixing_mode == 'fast' else LongBlendMode()
        arrangement = self.continuous_arrangement.stitch_tracks(
            selected_tracks, mixing
        )
        
        # Generate drum fills
        fills = []
        for sections in tagged_sections:
            track_fills = self.fill_injector.inject_fills_for_track(sections)
            fills.append(track_fills)
            
        # Generate crowd overlays
        overlays = self.crowd_overlay.generate_overlays_for_set(
            selected_tracks, tagged_sections
        )
        
        # Build result
        return {
            'template': template.to_dict(),
            'energy_curve': energy_curve.to_dict(),
            'tracks': [t.to_dict() for t in selected_tracks],
            'coherence_validation': coherence_result,
            'sections': [[s.to_dict() for s in secs] for secs in tagged_sections],
            'hot_cues': hot_cues,
            'arrangement': arrangement,
            'drum_fills': fills,
            'crowd_overlays': overlays,
            'total_duration_hours': template.duration_hours,
            'generation_timestamp': datetime.now().isoformat()
        }
    
    def generate_warmup_set(self, duration_hours: float = 1.5,
                         start_genre: str = 'deep-house') -> Dict[str, Any]:
        """
        T050: Generate a warm-up set with proper energy ramp.
        """
        warmup_logic = WarmupSetLogic(duration_hours)
        
        # Create a copy of the template by creating a new instance with modified values
        base_template = SET_TEMPLATES['warmup_90min']
        template = SetTemplate(
            name=base_template.name,
            duration_hours=base_template.duration_hours,
            duration_beats=base_template.duration_beats,
            num_tracks=base_template.num_tracks,
            avg_track_length_bars=base_template.avg_track_length_bars,
            transition_style=base_template.transition_style,
            energy_curve_type='ramp_up',  # Modified
            bpm_range=base_template.bpm_range,
            description=base_template.description
        )
        
        energy_curve = warmup_logic.generate_warmup_curve()
        
        return self.generate_set(
            template_name='warmup_90min',
            genre=start_genre,
            energy_curve_type='ramp_up'
        )
    
    # T060: Integration test entry point
    def run_integration_test_30min_mountain(self) -> Dict[str, Any]:
        """
        T060: Run 30-minute "Mountain" set integration test.
        
        Creates a showcase set demonstrating all ARC 3 features.
        """
        print("[T060] Running 30-min Mountain Set Integration Test")
        
        # Use showcase template
        result = self.generate_set(
            template_name='30min_showcase',
            genre='techno',
            start_bpm=132.0,
            energy_curve_type='mountain',
            mixing_mode='fast'
        )
        
        # Validate all components
        validation = self._validate_integration_test(result)
        result['integration_validation'] = validation
        
        return result
    
    def _validate_integration_test(self, result: Dict[str, Any]) -> Dict[str, Any]:
        """Validate the integration test result."""
        checks = {
            'has_tracks': len(result.get('tracks', [])) > 0,
            'has_energy_curve': 'energy_curve' in result,
            'has_arrangement': 'arrangement' in result,
            'has_coherence_check': result.get('coherence_validation', {}).get('valid', False),
            'has_hot_cues': len(result.get('hot_cues', [])) > 0,
            'has_drum_fills': len(result.get('drum_fills', [])) > 0,
            'template_valid': result.get('template', {}).get('duration_hours') == 0.5,
        }
        
        return {
            'all_passed': all(checks.values()),
            'checks': checks,
            'summary': f"{sum(checks.values())}/{len(checks)} checks passed"
        }


# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================

def get_available_templates() -> List[Dict[str, Any]]:
    """Get list of available set templates."""
    return [t.to_dict() for t in SET_TEMPLATES.values()]

def get_energy_curve_types() -> List[str]:
    """Get available energy curve types."""
    return EnergyCurve.CURVE_TYPES

def create_set_generator(library_path: Optional[str] = None) -> SetGenerator:
    """Factory function to create a SetGenerator instance."""
    return SetGenerator(library_path)


# =============================================================================
# COMMAND LINE INTERFACE
# =============================================================================

if __name__ == '__main__':
    import argparse
    
    parser = argparse.ArgumentParser(description='ARC 3 Set Generator')
    parser.add_argument('--template', default='30min_showcase',
                       help='Template to use')
    parser.add_argument('--genre', default='techno',
                       help='Genre for set')
    parser.add_argument('--test', action='store_true',
                       help='Run integration test (T060)')
    parser.add_argument('--list-templates', action='store_true',
                       help='List available templates')
    parser.add_argument('--warmup', action='store_true',
                       help='Generate warm-up set (T050)')
    
    args = parser.parse_args()
    
    if args.list_templates:
        templates = get_available_templates()
        print("Available Templates:")
        for t in templates:
            print(f"  - {t['name']}: {t['description']}")
            print(f"    Duration: {t['duration_hours']}h, Tracks: {t['num_tracks']}")
    elif args.test:
        gen = create_set_generator()
        result = gen.run_integration_test_30min_mountain()
        print(json.dumps(result['integration_validation'], indent=2))
    elif args.warmup:
        gen = create_set_generator()
        result = gen.generate_warmup_set()
        print(f"Warm-up set generated: {len(result.get('tracks', []))} tracks")
    else:
        gen = create_set_generator()
        result = gen.generate_set(
            template_name=args.template,
            genre=args.genre
        )
        print(f"Set generated: {len(result.get('tracks', []))} tracks")
        print(f"Coherence: {result.get('coherence_validation', {}).get('coherence_score', 0)}")

