"""
audio_key_compatibility.py - Key Compatibility Matrix y Tonal Analysis
FASE 4: T051-T062
"""
import logging
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass

logger = logging.getLogger("KeyCompatibility")


@dataclass
class KeyCompatibility:
    """Representa compatibilidad entre dos keys."""
    key1: str
    key2: str
    semitone_distance: int
    compatibility_score: float  # 0.0 - 1.0
    relationship: str  # 'same', 'fifth', 'relative', 'parallel', 'distant'


class KeyCompatibilityMatrix:
    """
    T052: Matriz completa de compatibilidad de keys musicales.

    Implementa relaciones armónicas basadas en:
    - Distancia de quintas (Circle of Fifths)
    - Relativos mayor/menor
    - Paralelos mayor/menor
    - Distancia en semitonos
    """

    # Circle of Fifths: orden de keys por quintas
    CIRCLE_OF_FIFTHS_MAJOR = [
        'C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#',  # Sharps side
        'Ab', 'Eb', 'Bb', 'F'  # Flats side
    ]

    CIRCLE_OF_FIFTHS_MINOR = [
        'Am', 'Em', 'Bm', 'F#m', 'C#m', 'G#m', 'Ebm', 'Bbm',  # Sharps side
        'Fm', 'Cm', 'Gm', 'Dm'  # Flats side
    ]

    # Relativos mayor/menor
    RELATIVE_KEYS = {
        'C': 'Am', 'G': 'Em', 'D': 'Bm', 'A': 'F#m',
        'E': 'C#m', 'B': 'G#m', 'F#': 'Ebm', 'C#': 'Bbm',
        'Ab': 'Fm', 'Eb': 'Cm', 'Bb': 'Gm', 'F': 'Dm',
        'Am': 'C', 'Em': 'G', 'Bm': 'D', 'F#m': 'A',
        'C#m': 'E', 'G#m': 'B', 'Ebm': 'F#', 'Bbm': 'C#',
        'Fm': 'Ab', 'Cm': 'Eb', 'Gm': 'Bb', 'Dm': 'F'
    }

    # Paralelos mayor/menor (misma tonic, diferente modo)
    PARALLEL_KEYS = {
        'C': 'Cm', 'G': 'Gm', 'D': 'Dm', 'A': 'Am',
        'E': 'Em', 'B': 'Bm', 'F#': 'F#m', 'C#': 'C#m',
        'Ab': 'Abm', 'Eb': 'Ebm', 'Bb': 'Bbm', 'F': 'Fm'
    }

    # Notas a índices cromáticos
    NOTE_INDEX = {
        'C': 0, 'C#': 1, 'Db': 1, 'D': 2, 'D#': 3, 'Eb': 3,
        'E': 4, 'F': 5, 'F#': 6, 'Gb': 6, 'G': 7, 'G#': 8,
        'Ab': 8, 'A': 9, 'A#': 10, 'Bb': 10, 'B': 11
    }

    def __init__(self):
        self._matrix: Dict[Tuple[str, str], float] = {}
        self._build_matrix()

    def _build_matrix(self):
        """Construye la matriz completa de compatibilidad."""
        all_keys = self.CIRCLE_OF_FIFTHS_MAJOR + self.CIRCLE_OF_FIFTHS_MINOR

        for key1 in all_keys:
            for key2 in all_keys:
                if key1 == key2:
                    score = 1.0
                else:
                    score = self._calculate_compatibility(key1, key2)
                self._matrix[(key1, key2)] = score

    def _calculate_compatibility(self, key1: str, key2: str) -> float:
        """
        Calcula score de compatibilidad entre dos keys.

        Scores basados en teoría musical:
        - Misma key: 1.0
        - Quinta directa: 0.95
        - Relativo mayor/menor: 0.90
        - Paralelo mayor/menor: 0.85
        - 2 quintas de distancia: 0.80
        - 3 quintas de distancia: 0.70
        - 4+ quintas: 0.50
        - Tritono (6 semitonos): 0.30
        - Más lejos: 0.10-0.20
        """
        # Check same key
        if key1 == key2:
            return 1.0

        # Check relativo
        if self.RELATIVE_KEYS.get(key1) == key2:
            return 0.90

        # Check paralelo
        if self.PARALLEL_KEYS.get(key1) == key2:
            return 0.85

        # Check quintas en circle of fifths
        distance_fifths = self._circle_distance(key1, key2)
        if distance_fifths == 1:
            return 0.95
        elif distance_fifths == 2:
            return 0.80
        elif distance_fifths == 3:
            return 0.70
        elif distance_fifths >= 4:
            return max(0.20, 0.70 - (distance_fifths - 3) * 0.10)

        # Semitone distance fallback
        semitone_dist = self._semitone_distance(key1, key2)
        if semitone_dist == 6:  # Tritono
            return 0.30
        elif semitone_dist <= 2:
            return 0.75
        elif semitone_dist <= 4:
            return 0.60
        else:
            return 0.40

    def _circle_distance(self, key1: str, key2: str) -> int:
        """Calcula distancia en circle of fifths."""
        # Normalizar a mayores
        k1_major = self._to_major(key1)
        k2_major = self._to_major(key2)

        if k1_major not in self.CIRCLE_OF_FIFTHS_MAJOR or k2_major not in self.CIRCLE_OF_FIFTHS_MAJOR:
            return 99

        idx1 = self.CIRCLE_OF_FIFTHS_MAJOR.index(k1_major)
        idx2 = self.CIRCLE_OF_FIFTHS_MAJOR.index(k2_major)

        # Distancia circular
        dist = abs(idx1 - idx2)
        return min(dist, 12 - dist)

    def _to_major(self, key: str) -> str:
        """Convierte cualquier key a su equivalente mayor."""
        if key.endswith('m') and not key.endswith('M'):
            # Es menor, devolver relativo mayor
            return self.RELATIVE_KEYS.get(key, key[:-1])
        return key

    def _semitone_distance(self, key1: str, key2: str) -> int:
        """Calcula distancia en semitonos entre roots de keys."""
        # Extraer root note
        root1 = self._extract_root(key1)
        root2 = self._extract_root(key2)

        idx1 = self.NOTE_INDEX.get(root1, 0)
        idx2 = self.NOTE_INDEX.get(root2, 0)

        dist = abs(idx1 - idx2)
        return min(dist, 12 - dist)

    def _extract_root(self, key: str) -> str:
        """Extrae la nota root de una key (ej: 'C#m' -> 'C#')."""
        if len(key) >= 2 and key[1] in '#b':
            return key[:2]
        return key[0]

    def get_compatibility(self, key1: str, key2: str) -> float:
        """Obtiene score de compatibilidad entre dos keys."""
        return self._matrix.get((key1, key2), 0.0)

    def get_related_keys(self, key: str, min_score: float = 0.80) -> List[Tuple[str, float]]:
        """Retorna keys relacionadas con score >= min_score."""
        related = []
        all_keys = self.CIRCLE_OF_FIFTHS_MAJOR + self.CIRCLE_OF_FIFTHS_MINOR

        for other_key in all_keys:
            if other_key == key:
                continue
            score = self.get_compatibility(key, other_key)
            if score >= min_score:
                related.append((other_key, score))

        return sorted(related, key=lambda x: x[1], reverse=True)

    def get_compatibility_report(self, key1: str, key2: str) -> Dict:
        """
        Genera reporte completo de compatibilidad entre dos keys.

        Returns dict con:
        - compatibility_score: float 0-1
        - semitone_distance: int
        - relationship: str ('same', 'relative', 'parallel', 'fifth', 'distant')
        - compatible: bool
        """
        score = self.get_compatibility(key1, key2)
        semitone_dist = self._semitone_distance(key1, key2)
        fifth_dist = self._circle_distance(key1, key2)

        # Determinar relación
        if key1 == key2:
            relationship = "same"
        elif self.RELATIVE_KEYS.get(key1) == key2:
            relationship = "relative"
        elif self.PARALLEL_KEYS.get(key1) == key2:
            relationship = "parallel"
        elif fifth_dist == 1:
            relationship = "fifth"
        elif fifth_dist <= 2:
            relationship = "close_fifth"
        else:
            relationship = "distant"

        return {
            'key1': key1,
            'key2': key2,
            'compatibility_score': score,
            'semitone_distance': semitone_dist,
            'fifth_distance': fifth_dist,
            'relationship': relationship,
            'compatible': score >= 0.70
        }

    def suggest_key_change(self, current_key: str, direction: str = "fifth_up") -> Optional[str]:
        """
        T054: Sugiere cambio de key armónico.

        Args:
            current_key: Key actual
            direction: 'fifth_up', 'fifth_down', 'relative', 'parallel'

        Returns:
            Key sugerida o None
        """
        if direction == "fifth_up":
            # Subir quinta = más energía
            return self._shift_fifth(current_key, 1)
        elif direction == "fifth_down":
            # Bajar quinta = más suave
            return self._shift_fifth(current_key, -1)
        elif direction == "relative":
            # Cambio a relativo mayor/menor
            return self.RELATIVE_KEYS.get(current_key)
        elif direction == "parallel":
            # Cambio a paralelo
            return self.PARALLEL_KEYS.get(current_key)

        return None

    def _shift_fifth(self, key: str, steps: int) -> Optional[str]:
        """Desplaza key por N quintas."""
        major = self._to_major(key)
        if major not in self.CIRCLE_OF_FIFTHS_MAJOR:
            return None

        idx = self.CIRCLE_OF_FIFTHS_MAJOR.index(major)
        new_idx = (idx + steps) % 12
        new_major = self.CIRCLE_OF_FIFTHS_MAJOR[new_idx]

        # Preservar modo (mayor/menor)
        if key.endswith('m') and not key.endswith('M'):
            return self.RELATIVE_KEYS.get(new_major, new_major.lower())
        return new_major

    def validate_key_match(self, sample_key: str, project_key: str,
                           tolerance: float = 0.70) -> bool:
        """
        T055: Valida si un sample es compatible con el proyecto.

        Args:
            sample_key: Key del sample
            project_key: Key del proyecto
            tolerance: Score mínimo de compatibilidad (default 0.70)

        Returns:
            True si es compatible
        """
        if not sample_key or not project_key:
            return True  # Sin info de key, asumir compatible

        score = self.get_compatibility(sample_key, project_key)
        return score >= tolerance


class TonalAnalyzer:
    """
    T060-T062: Análisis tonal y espectral.
    """

    # Rangos de brillo óptimos por rol (T056)
    BRIGHTNESS_RANGES = {
        'sub_bass': (0, 100),      # Muy oscuro
        'bass': (100, 500),        # Oscuro
        'kick': (200, 1000),       # Low-mid
        'pad': (500, 3000),        # Mid
        'chords': (800, 4000),     # Mid-high
        'lead': (1000, 6000),      # High
        'pluck': (1500, 5000),     # High-mid
        'atmos': (300, 8000),      # Variable
        'fx': (500, 10000),        # Variable
    }

    # Tags de color espectral (T061)
    SPECTRAL_TAGS = {
        'dark': (0, 500),
        'warm': (500, 1500),
        'neutral': (1500, 3000),
        'bright': (3000, 6000),
        'harsh': (6000, 20000)
    }

    def __init__(self):
        self.key_matrix = KeyCompatibilityMatrix()

    def analyze_spectral_fit(self, spectral_centroid: float, role: str) -> float:
        """
        T057: Calcula qué tan bien el brillo espectral se ajusta al rol.

        Args:
            spectral_centroid: Hz
            role: Rol del sample

        Returns:
            Score 0.0-1.0 de ajuste espectral
        """
        range_vals = self.BRIGHTNESS_RANGES.get(role, (0, 10000))
        min_val, max_val = range_vals

        if min_val <= spectral_centroid <= max_val:
            return 1.0

        # Fuera de rango: calcular penalización
        if spectral_centroid < min_val:
            diff = min_val - spectral_centroid
        else:
            diff = spectral_centroid - max_val

        # Penalización proporcional
        penalty = min(1.0, diff / 2000.0)
        return max(0.0, 1.0 - penalty)

    def tag_spectral_color(self, spectral_centroid: float) -> str:
        """
        T061: Asigna tag de color espectral.

        Returns:
            'dark', 'warm', 'neutral', 'bright', 'harsh'
        """
        for tag, (min_hz, max_hz) in self.SPECTRAL_TAGS.items():
            if min_hz <= spectral_centroid <= max_hz:
                return tag
        return 'unknown'

    def get_key_compatibility_report(self, key1: str, key2: str) -> Dict:
        """Genera reporte completo de compatibilidad."""
        score = self.key_matrix.get_compatibility(key1, key2)
        related = self.key_matrix.get_related_keys(key1, min_score=0.70)

        return {
            'key1': key1,
            'key2': key2,
            'compatibility_score': round(score, 2),
            'compatible': score >= 0.70,
            'related_keys': related[:5],
            'suggested_changes': {
                'fifth_up': self.key_matrix.suggest_key_change(key1, 'fifth_up'),
                'fifth_down': self.key_matrix.suggest_key_change(key1, 'fifth_down'),
                'relative': self.key_matrix.suggest_key_change(key1, 'relative'),
                'parallel': self.key_matrix.suggest_key_change(key1, 'parallel')
            }
        }


# Instancia global
_key_matrix: Optional[KeyCompatibilityMatrix] = None
_tonal_analyzer: Optional[TonalAnalyzer] = None


def get_key_matrix() -> KeyCompatibilityMatrix:
    """Obtiene instancia global de la matriz de compatibilidad."""
    global _key_matrix
    if _key_matrix is None:
        _key_matrix = KeyCompatibilityMatrix()
    return _key_matrix


def get_tonal_analyzer() -> TonalAnalyzer:
    """Obtiene instancia global del analizador tonal."""
    global _tonal_analyzer
    if _tonal_analyzer is None:
        _tonal_analyzer = TonalAnalyzer()
    return _tonal_analyzer
