"""
T096-T223: DJ Set Mapper - Generación de Sets Multihour
Mapeo completo para sets DJ de varias horas
"""

import json
import os
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum


class SetEvolution(Enum):
    """Tipos de evolución del set."""
    PROGRESSIVE = "progressive"  # De deep a peak time
    PEAK_TIME = "peak_time"  # Toda energía alta
    WARMUP = "warmup"  # Inicio suave, construcción gradual
    STORY = "story"  # Narrativa musical
    HYBRID = "hybrid"  # Mix de estilos


@dataclass
class TrackBlueprint:
    """Blueprint para un track en el set."""
    index: int
    genre: str
    style: str
    bpm: int
    key: str
    energy_level: int  # 1-10
    duration_minutes: float
    transition_in: str
    transition_out: str
    palette_drums: Optional[str] = None
    palette_bass: Optional[str] = None
    palette_music: Optional[str] = None


class DJSetMapper:
    """
    Mapeador completo para sets DJ multihour.
    
    T096: Genera sets DJ completos conectados con Palette Lock.
    T223: Mapeo completo para transiciones y energía.
    """
    
    # Configuraciones por estilo de evolución
    EVOLUTION_PROFILES = {
        SetEvolution.PROGRESSIVE: {
            'energy_curve': [3, 4, 5, 6, 7, 8, 9, 8, 7, 6],  # Subida y bajada
            'bpm_progression': [118, 120, 122, 124, 126, 128, 130, 128, 126, 124],
            'genre_progression': ['deep-house', 'house', 'tech-house', 'techno', 'techno', 
                                 'techno', 'peak-techno', 'techno', 'tech-house', 'house']
        },
        SetEvolution.PEAK_TIME: {
            'energy_curve': [8, 9, 9, 10, 10, 9, 9, 8],
            'bpm_progression': [128, 130, 132, 135, 138, 136, 134, 132],
            'genre_progression': ['techno', 'techno', 'hard-techno', 'hard-techno',
                                 'peak-techno', 'techno', 'techno', 'techno']
        },
        SetEvolution.WARMUP: {
            'energy_curve': [2, 3, 4, 5, 6, 7, 8, 7],
            'bpm_progression': [115, 118, 120, 122, 124, 126, 128, 126],
            'genre_progression': ['ambient', 'deep-house', 'deep-house', 'house',
                                 'tech-house', 'techno', 'techno', 'techno']
        },
        SetEvolution.STORY: {
            'energy_curve': [3, 4, 6, 8, 7, 9, 6, 4, 3],
            'bpm_progression': [120, 122, 124, 128, 126, 130, 124, 120, 118],
            'genre_progression': ['downtempo', 'deep-house', 'house', 'techno',
                                 'melodic-techno', 'peak-techno', 'tech-house', 'deep-house', 'ambient']
        },
        SetEvolution.HYBRID: {
            'energy_curve': [5, 7, 6, 8, 7, 9, 8, 6],
            'bpm_progression': [124, 126, 125, 128, 127, 130, 128, 126],
            'genre_progression': ['tech-house', 'techno', 'house', 'techno',
                                 'tech-house', 'techno', 'techno', 'tech-house']
        }
    }
    
    # Duraciones típicas por track
    TRACK_DURATION_RANGES = {
        'short': (4.0, 6.0),      # 4-6 minutos
        'standard': (6.0, 8.0),   # 6-8 minutos
        'extended': (8.0, 12.0),  # 8-12 minutos
        'long': (10.0, 16.0)      # 10-16 minutos (sets prog)
    }
    
    def __init__(self, duration_hours: float = 2.0,
                 evolution: SetEvolution = SetEvolution.PROGRESSIVE,
                 track_duration_type: str = 'standard'):
        self.duration_hours = max(0.5, min(4.0, duration_hours))
        self.evolution = evolution
        self.track_duration_range = self.TRACK_DURATION_RANGES.get(track_duration_type, (6.0, 8.0))
        self.profile = self.EVOLUTION_PROFILES[evolution]
        
    def generate_set_blueprint(self) -> Dict[str, Any]:
        """Genera blueprint completo del set DJ."""
        # Calcular número de tracks
        avg_track_duration = sum(self.track_duration_range) / 2
        target_duration_minutes = self.duration_hours * 60
        num_tracks = int(target_duration_minutes / avg_track_duration)
        
        # Ajustar curvas de energía/BPM al número de tracks
        energy_curve = self._interpolate_curve(self.profile['energy_curve'], num_tracks)
        bpm_curve = self._interpolate_curve(self.profile['bpm_progression'], num_tracks)
        genre_curve = self._interpolate_genres(self.profile['genre_progression'], num_tracks)
        
        # Generar tracks
        tracks = []
        current_time = 0.0
        
        # Keys armónicamente relacionadas (circle of fifths)
        key_progression = self._generate_key_progression(num_tracks)
        
        for i in range(num_tracks):
            track_duration = self._get_track_duration(i, num_tracks)
            
            track = TrackBlueprint(
                index=i,
                genre=genre_curve[i],
                style=self._get_style_for_position(i, num_tracks, genre_curve[i]),
                bpm=int(bpm_curve[i]),
                key=key_progression[i],
                energy_level=int(energy_curve[i]),
                duration_minutes=track_duration,
                transition_in='fade' if i > 0 else 'start',
                transition_out='mix' if i < num_tracks - 1 else 'end',
                palette_drums=None,  # Se asignará durante generación
                palette_bass=None,
                palette_music=None
            )
            
            tracks.append(track)
            current_time += track_duration
        
        # Calcular transiciones y palette locks
        self._calculate_transitions(tracks)
        self._assign_palette_locks(tracks)
        
        return {
            'set_id': f'djset_{datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'duration_hours': self.duration_hours,
            'evolution_type': self.evolution.value,
            'total_tracks': len(tracks),
            'estimated_duration_minutes': sum(t.duration_minutes for t in tracks),
            'tracks': [
                {
                    'index': t.index,
                    'genre': t.genre,
                    'style': t.style,
                    'bpm': t.bpm,
                    'key': t.key,
                    'energy_level': t.energy_level,
                    'duration_minutes': t.duration_minutes,
                    'transition_in': t.transition_in,
                    'transition_out': t.transition_out,
                    'start_time_minutes': sum(tracks[j].duration_minutes for j in range(t.index)),
                    'palette_lock': {
                        'drums': t.palette_drums,
                        'bass': t.palette_bass,
                        'music': t.palette_music
                    }
                }
                for t in tracks
            ],
            'key_relationships': self._analyze_key_relationships(tracks),
            'energy_arc': {
                'start': tracks[0].energy_level if tracks else 0,
                'peak': max(t.energy_level for t in tracks) if tracks else 0,
                'end': tracks[-1].energy_level if tracks else 0,
                'average': sum(t.energy_level for t in tracks) / len(tracks) if tracks else 0
            },
            'bpm_range': {
                'min': min(t.bpm for t in tracks) if tracks else 0,
                'max': max(t.bpm for t in tracks) if tracks else 0,
                'average': sum(t.bpm for t in tracks) / len(tracks) if tracks else 0
            }
        }
    
    def _interpolate_curve(self, curve: List[int], target_length: int) -> List[int]:
        """Interpola una curva a la longitud objetivo."""
        if len(curve) >= target_length:
            return curve[:target_length]
        
        result = []
        step = len(curve) / target_length
        
        for i in range(target_length):
            idx = int(i * step)
            idx = min(idx, len(curve) - 1)
            result.append(curve[idx])
        
        return result
    
    def _interpolate_genres(self, genres: List[str], target_length: int) -> List[str]:
        """Interpola géneros a la longitud objetivo."""
        if len(genres) >= target_length:
            return genres[:target_length]
        
        result = []
        step = len(genres) / target_length
        
        for i in range(target_length):
            idx = int(i * step)
            idx = min(idx, len(genres) - 1)
            result.append(genres[idx])
        
        return result
    
    def _generate_key_progression(self, num_tracks: int) -> List[str]:
        """Genera progresión de keys armónicamente relacionadas."""
        # Circle of fifths - progresión musical lógica
        keys = ['Am', 'Em', 'Bm', 'F#m', 'C#m', 'G#m', 'D#m', 'A#m',
                'Fm', 'Cm', 'Gm', 'Dm']
        
        # Empezar en posición aleatoria pero musical
        start_idx = 0  # Podría ser aleatorio
        
        progression = []
        current_idx = start_idx
        
        for i in range(num_tracks):
            progression.append(keys[current_idx % len(keys)])
            # Mover en el círculo de quintas (saltos de +7 semitonos = +5 posiciones)
            # O movimientos cercanos para transiciones suaves
            if i % 3 == 0:
                current_idx += 1  # Movimiento suave
            else:
                current_idx += 5  # Cambio de energía
        
        return progression
    
    def _get_track_duration(self, index: int, total: int) -> float:
        """Determina duración de un track según posición."""
        min_dur, max_dur = self.track_duration_range
        
        # Tracks de apertura y cierre pueden ser más cortos
        if index == 0 or index == total - 1:
            return min_dur + 1.0
        
        # Tracks del medio pueden ser más largos
        if 0.3 < index / total < 0.7:
            return max_dur
        
        # Duración estándar
        return (min_dur + max_dur) / 2
    
    def _get_style_for_position(self, index: int, total: int, genre: str) -> str:
        """Determina el estilo según posición en el set."""
        position = index / total
        
        if position < 0.2:
            return 'intro' if 'ambient' in genre or 'deep' in genre else 'warmup'
        elif position < 0.4:
            return 'building'
        elif position < 0.6:
            return 'peak' if self.evolution in [SetEvolution.PEAK_TIME, SetEvolution.PROGRESSIVE] else 'groove'
        elif position < 0.8:
            return 'peak' if self.evolution == SetEvolution.PEAK_TIME else 'sustained'
        else:
            return 'cooldown' if self.evolution == SetEvolution.PROGRESSIVE else 'outro'
    
    def _calculate_transitions(self, tracks: List[TrackBlueprint]):
        """Calcula tipos de transición entre tracks."""
        for i in range(len(tracks) - 1):
            current = tracks[i]
            next_track = tracks[i + 1]
            
            # Determinar tipo de transición según cambios
            bpm_diff = abs(next_track.bpm - current.bpm)
            energy_diff = next_track.energy_level - current.energy_level
            
            if bpm_diff > 5:
                current.transition_out = 'ramp'
                next_track.transition_in = 'catch_up'
            elif energy_diff > 2:
                current.transition_out = 'build'
                next_track.transition_in = 'drop'
            elif energy_diff < -2:
                current.transition_out = 'breakdown'
                next_track.transition_in = 'recover'
            else:
                current.transition_out = 'smooth_mix'
                next_track.transition_in = 'smooth_mix'
    
    def _assign_palette_locks(self, tracks: List[TrackBlueprint]):
        """Asigna palette locks para coherencia entre tracks relacionados."""
        # Agrupar tracks por género similar
        genre_groups = {}
        for track in tracks:
            base_genre = track.genre.split('-')[0]  # 'deep-house' -> 'deep'
            if base_genre not in genre_groups:
                genre_groups[base_genre] = []
            genre_groups[base_genre].append(track)
        
        # Asignar palettes por grupo
        for genre, group_tracks in genre_groups.items():
            if len(group_tracks) >= 2:
                # Todos los tracks del grupo comparten palette
                palette_drums = f'librerias/all_tracks/{genre.title()}/Drums'
                palette_bass = f'librerias/all_tracks/{genre.title()}/Bass'
                palette_music = f'librerias/all_tracks/{genre.title()}/Synths'
                
                for track in group_tracks:
                    track.palette_drums = palette_drums
                    track.palette_bass = palette_bass
                    track.palette_music = palette_music
    
    def _analyze_key_relationships(self, tracks: List[TrackBlueprint]) -> List[Dict[str, Any]]:
        """Analiza relaciones armónicas entre tracks consecutivos."""
        relationships = []
        
        for i in range(len(tracks) - 1):
            current_key = tracks[i].key
            next_key = tracks[i + 1].key
            
            # Determinar tipo de relación
            if current_key == next_key:
                relation = 'same_key'
            elif self._is_relative(current_key, next_key):
                relation = 'relative'
            elif self._is_fifth(current_key, next_key):
                relation = 'fifth'
            elif self._is_semitone(current_key, next_key):
                relation = 'semitone'
            else:
                relation = 'other'
            
            relationships.append({
                'from_track': i,
                'to_track': i + 1,
                'from_key': current_key,
                'to_key': next_key,
                'relationship': relation,
                'compatibility': 'high' if relation in ['same_key', 'relative', 'fifth'] else 'medium'
            })
        
        return relationships
    
    def _is_relative(self, key1: str, key2: str) -> bool:
        """Verifica si dos keys son relativas (mayor/menor)."""
        # Simplificado - implementación real usaría teoría musical
        relatives = {
            'Am': 'C', 'C': 'Am',
            'Em': 'G', 'G': 'Em',
            'Bm': 'D', 'D': 'Bm',
            'F#m': 'A', 'A': 'F#m'
        }
        return relatives.get(key1) == key2
    
    def _is_fifth(self, key1: str, key2: str) -> bool:
        """Verifica si las keys están a una quinta de distancia."""
        # Simplificado
        return False  # Implementar con círculo de quintas
    
    def _is_semitone(self, key1: str, key2: str) -> bool:
        """Verifica si las keys están a un semitono."""
        # Simplificado
        return False


def generate_dj_set(duration_hours: float = 1.0,
                   style_evolution: str = 'progressive') -> Dict[str, Any]:
    """
    T096: Genera un set DJ completo de N horas.
    
    Genera múltiples tracks conectados con Palette Lock linked entre sí.
    
    Args:
        duration_hours: Duración del set (0.5 - 4.0 horas)
        style_evolution: 'progressive', 'peak_time', 'warmup', 'story', 'hybrid'
        
    Returns:
        Blueprint completo del set DJ
    """
    evolution_map = {
        'progressive': SetEvolution.PROGRESSIVE,
        'peak_time': SetEvolution.PEAK_TIME,
        'warmup': SetEvolution.WARMUP,
        'story': SetEvolution.STORY,
        'hybrid': SetEvolution.HYBRID
    }
    
    evolution = evolution_map.get(style_evolution, SetEvolution.PROGRESSIVE)
    mapper = DJSetMapper(duration_hours=duration_hours, evolution=evolution)
    
    return mapper.generate_set_blueprint()


if __name__ == '__main__':
    # Test del DJ Set Mapper
    for evolution in ['progressive', 'peak_time', 'warmup']:
        blueprint = generate_dj_set(duration_hours=1.0, style_evolution=evolution)
        print(f"\n=== {evolution.upper()} SET ===")
        print(f"Tracks: {blueprint['total_tracks']}")
        print(f"Duration: {blueprint['estimated_duration_minutes']:.1f} minutes")
        print(f"Energy Arc: {blueprint['energy_arc']}")
        print(f"BPM Range: {blueprint['bpm_range']}")
