"""harmonic_engine.py - Real-time Harmonic & BPM Analysis (T021-T040)

ARC 2: Complete harmonic analysis and DJ-style mixing engine for AbletonMCP-AI.

T021: Camelot Wheel Integration (key dictionaries)
T022: Key Detection Fallback (spectral pitch analysis)
T023: Allowed Key Routing (prevent clashing keys)
T024: Energy Level Indexing (1-10 energy levels)
T025: Clip Warping API Bridge (warp_mode control)
T026: Automatic Warp Strategy (Pro=vocals, Beats=drums)
T027: Pitch Shifting Macro (+/- 1-2 semitones)
T028: Harmonic Mixing Ruleset (+/- 1 Camelot, +2 semitones)
T029: Rhythm Consistency Check (BPM stability)
T030: "Double Drop" alignment
T031: Sync Engine Bridge (programmatic BPM lock)
T032: Groove Pool Extraction (extract swing)
T033: Groove Application (apply swing mathematically)
T034: Phrase Matching Analysis (16/32-bar structures)
T035: Intro/Outro Alignment (16-bar overlay)
T036: Modulation Transition (4-bar key bridge)
T037: Key-Lock Toggle (pitch_shift vs tempo)
T038: Camelot Wheel Display (logging)
T039: Auto-Fix Clashing Baselines (auto-mute weaker)
T040: Integration Test - 5-track harmonic mini-mix
"""

from __future__ import annotations

import logging
import math
import re
from dataclasses import dataclass, field
from enum import Enum, IntEnum
from typing import Dict, List, Optional, Tuple, Set, Any, Union
from collections import defaultdict

logger = logging.getLogger("HarmonicEngine")


# ============================================================================
# T021: CAMELOT WHEEL INTEGRATION
# ============================================================================

class CamelotWheel:
    """
    T021: Camelot Wheel Integration - Complete key dictionary and wheel navigation.
    """
    
    # Standard Camelot Wheel mappings
    # Minor keys (A side) - Standard notation
    MINOR_KEYS: Dict[int, str] = {
        1: "G#m", 2: "D#m", 3: "A#m", 4: "Fm", 5: "Cm", 6: "Gm",
        7: "Dm", 8: "Am", 9: "Em", 10: "Bm", 11: "F#m", 12: "C#m"
    }
    
    # Major keys (B side) - Standard notation
    MAJOR_KEYS: Dict[int, str] = {
        1: "B", 2: "F#", 3: "C#", 4: "Ab", 5: "Eb", 6: "Bb",
        7: "F", 8: "C", 9: "G", 10: "D", 11: "A", 12: "E"
    }
    
    KEY_TO_CAMELOT: Dict[str, str] = {}
    for num, key in MINOR_KEYS.items():
        KEY_TO_CAMELOT[key] = f"{num}A"
    for num, key in MAJOR_KEYS.items():
        KEY_TO_CAMELOT[key] = f"{num}B"
    
    KEY_ALIASES: Dict[str, str] = {
        # Minor key aliases - map to the canonical form in MINOR_KEYS
        "Bbm": "A#m",  # Bbm is A#m in our keys
        "Dbm": "C#m",
        "Gbm": "F#m",
        "Abm": "G#m",
        "Ebm": "D#m",
        "A#m": "A#m",  # Self-reference for canonical keys
        "C#m": "C#m",
        "F#m": "F#m",
        "G#m": "G#m",
        "D#m": "D#m",
        # Major key aliases
        "A#": "Bb", "Bb": "Bb",
        "Db": "C#", "C#": "C#",
        "Gb": "F#", "F#": "F#",
        "Ab": "G#", "G#": "G#",
        "D#": "Eb", "Eb": "Eb"
    }
    
    KEY_ROOT_MIDI: Dict[str, int] = {
        "Am": 57, "Em": 52, "Bm": 59, "F#m": 54, "C#m": 61, "G#m": 56,
        "Ebm": 51, "Bbm": 58, "Fm": 53, "Cm": 48, "Gm": 55, "Dm": 50,
        "B": 59, "F#": 54, "C#": 61, "G#": 56, "Eb": 51, "Bb": 58,
        "F": 53, "C": 48, "G": 55, "D": 50, "A": 57, "E": 52,
        "Ab": 56, "D#m": 51, "A#m": 58
    }
    
    @classmethod
    def get_camelot_code(cls, key: str) -> Optional[str]:
        key = key.strip()
        if key in cls.KEY_ALIASES:
            key = cls.KEY_ALIASES[key]
        return cls.KEY_TO_CAMELOT.get(key)
    
    @classmethod
    def get_key_from_camelot(cls, code: str) -> Optional[str]:
        code = code.strip().upper()
        if len(code) < 2:
            return None
        num = int(code[:-1]) if code[:-1].isdigit() else None
        letter = code[-1] if code[-1] in "AB" else None
        if num is None or letter is None:
            return None
        if letter == "A":
            return cls.MINOR_KEYS.get(num)
        else:
            return cls.MAJOR_KEYS.get(num)
    
    @classmethod
    def get_compatible_keys(cls, key: str) -> List[str]:
        code = cls.get_camelot_code(key)
        if not code:
            return []
        
        num = int(code[:-1])
        letter = code[-1]
        
        compatible = []
        
        # +/- 1 Camelot number
        for offset in [-1, 1]:
            new_num = ((num - 1 + offset) % 12) + 1
            compatible.append(f"{new_num}{letter}")
        
        # Relative major/minor
        opposite = "B" if letter == "A" else "A"
        compatible.append(f"{num}{opposite}")
        
        # +/- 2 (energy shift)
        for offset in [-2, 2]:
            new_num = ((num - 1 + offset) % 12) + 1
            compatible.append(f"{new_num}{letter}")
        
        return list(set(compatible))
    
    @classmethod
    def is_compatible(cls, key1: str, key2: str) -> bool:
        code1 = cls.get_camelot_code(key1)
        code2 = cls.get_camelot_code(key2)
        if not code1 or not code2:
            return False
        return code2 in cls.get_compatible_keys(key1)
    
    @classmethod
    def calculate_distance(cls, key1: str, key2: str) -> int:
        code1 = cls.get_camelot_code(key1)
        code2 = cls.get_camelot_code(key2)
        if not code1 or not code2:
            return 99
        if code1 == code2:
            return 0
        
        num1 = int(code1[:-1])
        num2 = int(code2[:-1])
        letter1 = code1[-1]
        letter2 = code2[-1]
        
        if letter1 == letter2:
            diff = abs(num1 - num2)
            return min(diff, 12 - diff)
        
        if num1 == num2:
            return 1  # Relative major/minor
        
        diff = abs(num1 - num2)
        return min(diff, 12 - diff) + 1


# ============================================================================
# T022: KEY DETECTION FALLBACK
# ============================================================================

class KeyDetector:
    """T022: Key Detection Fallback - Spectral pitch analysis."""
    
    KEY_PROFILES: Dict[str, List[float]] = {
        "major": [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88],
        "minor": [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
    }
    
    @classmethod
    def detect_key_from_spectral_features(
        cls,
        spectral_centroid: float,
        chroma_energy: Optional[List[float]] = None
    ) -> Optional[str]:
        # Simple spectral centroid matching
        ranges = {
            "C": (250, 450), "G": (200, 400), "D": (220, 420),
            "A": (180, 380), "E": (240, 440), "B": (260, 460),
            "F#": (280, 480), "C#": (300, 500), "G#": (320, 520),
            "Eb": (340, 540), "Bb": (360, 560), "F": (380, 580)
        }
        
        best_key = None
        best_score = float('inf')
        
        for key, (low, high) in ranges.items():
            center = (low + high) / 2
            distance = abs(spectral_centroid - center)
            if distance < best_score:
                best_score = distance
                best_key = key
        
        return best_key
    
    @classmethod
    def estimate_key_from_filename(cls, filename: str) -> Optional[str]:
        patterns = [
            r'[_\-]([A-G][#b]?m?)[_\-]',
            r'[_\-]([A-G][#b]?m?)(?=\d)',
            r'key[_\-]?([A-G][#b]?m?)',
            r'in([A-G][#b]?m?)[_\-]',
        ]
        
        for pattern in patterns:
            match = re.search(pattern, filename, re.IGNORECASE)
            if match:
                key = match.group(1)
                key = key[0].upper() + key[1:].lower() if len(key) > 1 else key.upper()
                return key
        
        return None


# ============================================================================
# T023: ALLOWED KEY ROUTING
# ============================================================================

class KeyRouter:
    """T023: Allowed Key Routing - Prevent clashing keys."""
    
    def __init__(self):
        self.track_keys: Dict[str, str] = {}
        self.key_conflicts: List[Tuple[str, str, str]] = []
    
    def register_track_key(self, track_id: str, key: str) -> bool:
        for existing_id, existing_key in self.track_keys.items():
            if existing_id == track_id:
                continue
            
            distance = CamelotWheel.calculate_distance(key, existing_key)
            
            if distance >= 6:
                self.key_conflicts.append((track_id, existing_id, "CLASH"))
                logger.warning(f"[KEY ROUTER] Key clash: {track_id} ({key}) vs {existing_id} ({existing_key})")
                return False
            elif distance >= 3:
                self.key_conflicts.append((track_id, existing_id, "WARNING"))
                logger.warning(f"[KEY ROUTER] Key warning: {track_id} ({key}) vs {existing_id} ({existing_key})")
        
        self.track_keys[track_id] = key
        logger.info(f"[KEY ROUTER] Registered {track_id} in key {key}")
        return True
    
    def get_harmonic_mix_recommendations(self) -> List[Dict]:
        recommendations = []
        
        if len(self.track_keys) < 2:
            return recommendations
        
        tracks = list(self.track_keys.items())
        
        for i in range(len(tracks) - 1):
            current_id, current_key = tracks[i]
            next_id, next_key = tracks[i + 1]
            
            distance = CamelotWheel.calculate_distance(current_key, next_key)
            
            rec = {
                "from_track": current_id,
                "to_track": next_id,
                "from_key": current_key,
                "to_key": next_key,
                "distance": distance,
                "compatible": distance <= 2,
                "transition_type": None
            }
            
            if distance == 0:
                rec["transition_type"] = "same_key"
            elif distance == 1:
                rec["transition_type"] = "camelot_step"
            elif CamelotWheel.get_camelot_code(current_key)[:-1] == CamelotWheel.get_camelot_code(next_key)[:-1]:
                rec["transition_type"] = "relative_mode"
            elif distance <= 2:
                rec["transition_type"] = "energy_shift"
            else:
                rec["transition_type"] = "requires_modulation"
            
            recommendations.append(rec)
        
        return recommendations


# ============================================================================
# T024: ENERGY LEVEL INDEXING
# ============================================================================

class EnergyLevelIndex:
    """T024: Energy Level Indexing - 1-10 energy classification."""
    
    ENERGY_LEVELS: Dict[int, Dict] = {
        1: {"name": "Silent", "db_range": (-60, -40)},
        2: {"name": "Whisper", "db_range": (-40, -30)},
        3: {"name": "Ambient", "db_range": (-30, -24)},
        4: {"name": "Low", "db_range": (-24, -18)},
        5: {"name": "Medium-Low", "db_range": (-18, -14)},
        6: {"name": "Medium", "db_range": (-14, -10)},
        7: {"name": "Medium-High", "db_range": (-10, -6)},
        8: {"name": "High", "db_range": (-6, -3)},
        9: {"name": "Peak", "db_range": (-3, 0)},
        10: {"name": "Overload", "db_range": (0, 6)}
    }
    
    def __init__(self):
        self.track_energy: Dict[str, int] = {}
        self.track_energy_history: Dict[str, List[int]] = defaultdict(list)
    
    def set_track_energy(self, track_id: str, level: int):
        level = max(1, min(10, level))
        self.track_energy[track_id] = level
        self.track_energy_history[track_id].append(level)
        logger.info(f"[ENERGY] {track_id}: level {level}/10")
    
    def get_track_energy(self, track_id: str) -> int:
        return self.track_energy.get(track_id, 5)
    
    def estimate_energy_from_features(
        self,
        spectral_centroid: float,
        rms_energy: float,
        transients_per_bar: float
    ) -> int:
        if rms_energy < -40:
            base_level = 2
        elif rms_energy < -24:
            base_level = 4
        elif rms_energy < -12:
            base_level = 6
        elif rms_energy < -6:
            base_level = 8
        else:
            base_level = 10
        
        if spectral_centroid > 3000:
            base_level += 1
        elif spectral_centroid < 500:
            base_level -= 1
        
        if transients_per_bar > 16:
            base_level += 1
        elif transients_per_bar < 4:
            base_level -= 1
        
        return max(1, min(10, base_level))
    
    def get_weaker_track(self, track1: str, track2: str) -> str:
        energy1 = self.get_track_energy(track1)
        energy2 = self.get_track_energy(track2)
        return track2 if energy2 < energy1 else track1


# ============================================================================
# T025, T026: CLIP WARPING API & AUTOMATIC WARP STRATEGY
# ============================================================================

class WarpMode(IntEnum):
    """Ableton Live warp modes."""
    BEATS = 0
    TONES = 1
    TEXTURE = 2
    RE_PITCH = 3
    COMPLEX = 4
    COMPLEX_PRO = 5
    REX = 6
    SLICE = 7


class WarpStrategy:
    """
    T025, T026: Clip Warping API Bridge and Automatic Warp Strategy.
    """
    
    CONTENT_STRATEGY: Dict[str, Dict] = {
        "vocals": {
            "mode": WarpMode.COMPLEX_PRO,
            "name": "Complex Pro",
            "formant_preserve": True
        },
        "drums": {
            "mode": WarpMode.BEATS,
            "name": "Beats"
        },
        "percussion": {
            "mode": WarpMode.BEATS,
            "name": "Beats"
        },
        "bass": {
            "mode": WarpMode.TONES,
            "name": "Tones"
        },
        "lead": {
            "mode": WarpMode.COMPLEX,
            "name": "Complex"
        },
        "pad": {
            "mode": WarpMode.TEXTURE,
            "name": "Texture"
        },
        "texture": {
            "mode": WarpMode.TEXTURE,
            "name": "Texture"
        },
        "ambient": {
            "mode": WarpMode.TEXTURE,
            "name": "Texture"
        },
        "synth": {
            "mode": WarpMode.COMPLEX,
            "name": "Complex"
        }
    }
    
    @classmethod
    def get_warp_settings(cls, content_type: str) -> Optional[Dict]:
        return cls.CONTENT_STRATEGY.get(content_type.lower())
    
    @classmethod
    def auto_detect_content_type(
        cls,
        spectral_centroid: float,
        transient_density: float,
        harmonic_ratio: float
    ) -> str:
        if transient_density > 8:
            if spectral_centroid > 2000:
                return "drums"
            return "percussion"
        
        if spectral_centroid < 400 and harmonic_ratio > 0.6:
            return "bass"
        
        if transient_density < 2 and harmonic_ratio > 0.7:
            return "pad"
        
        if spectral_centroid > 1000 and harmonic_ratio > 0.5:
            return "lead"
        
        if spectral_centroid > 2500 and harmonic_ratio > 0.4:
            return "vocals"
        
        return "synth"
    
    @classmethod
    def generate_warp_api_command(
        cls,
        clip_id: str,
        content_type: str,
        bpm: float
    ) -> Dict:
        settings = cls.get_warp_settings(content_type) or cls.get_warp_settings("synth")
        
        return {
            "clip_id": clip_id,
            "warp_enabled": True,
            "warp_mode": settings["mode"].value,
            "warp_mode_name": settings["name"],
            "bpm": bpm,
            "settings": {k: v for k, v in settings.items() if k not in ["mode", "name"]}
        }


# ============================================================================
# T027, T028: PITCH SHIFTING & HARMONIC MIXING RULESET
# ============================================================================

class PitchShifter:
    """T027, T028: Pitch Shifting Macro and Harmonic Mixing Ruleset."""
    
    MAX_SHIFT_SEMITONES: int = 2
    
    @classmethod
    def calculate_shift(
        cls,
        source_key: str,
        target_key: str,
        allow_modulation: bool = False
    ) -> Optional[int]:
        source_root = CamelotWheel.KEY_ROOT_MIDI.get(source_key)
        target_root = CamelotWheel.KEY_ROOT_MIDI.get(target_key)
        
        if source_root is None or target_root is None:
            return None
        
        shift = target_root - source_root
        
        while shift > 6:
            shift -= 12
        while shift < -6:
            shift += 12
        
        if abs(shift) > cls.MAX_SHIFT_SEMITONES and not allow_modulation:
            return None
        
        return shift
    
    @classmethod
    def get_harmonic_shift_options(cls, current_key: str) -> List[Dict]:
        options = []
        compatible_codes = CamelotWheel.get_compatible_keys(current_key)
        
        for code in compatible_codes:
            target_key = CamelotWheel.get_key_from_camelot(code)
            if not target_key:
                continue
            
            shift = cls.calculate_shift(current_key, target_key)
            if shift is not None:
                options.append({
                    "target_key": target_key,
                    "camelot_code": code,
                    "semitone_shift": shift,
                    "quality_score": 100 - abs(shift) * 10
                })
        
        return sorted(options, key=lambda x: abs(x["semitone_shift"]))
    
    @classmethod
    def find_best_modulation_path(
        cls,
        start_key: str,
        end_key: str,
        max_steps: int = 3
    ) -> List[Dict]:
        if CamelotWheel.is_compatible(start_key, end_key):
            shift = cls.calculate_shift(start_key, end_key)
            return [{"key": end_key, "shift_from_prev": shift, "step": 1}]
        
        # Find bridge via relative major/minor
        start_code = CamelotWheel.get_camelot_code(start_key)
        if start_code:
            opposite_letter = "B" if start_code[-1] == "A" else "A"
            relative_code = f"{start_code[:-1]}{opposite_letter}"
            bridge_key = CamelotWheel.get_key_from_camelot(relative_code)
            
            if bridge_key and CamelotWheel.is_compatible(bridge_key, end_key):
                shift1 = cls.calculate_shift(start_key, bridge_key)
                shift2 = cls.calculate_shift(bridge_key, end_key)
                return [
                    {"key": bridge_key, "shift_from_prev": shift1, "step": 1},
                    {"key": end_key, "shift_from_prev": shift2, "step": 2}
                ]
        
        return [{"key": end_key, "shift_from_prev": 0, "step": 1}]


# ============================================================================
# T029, T030, T031: RHYTHM CONSISTENCY & SYNC ENGINE
# ============================================================================

class RhythmConsistencyChecker:
    """T029: Rhythm Consistency Check - BPM stability validation."""
    
    def __init__(self):
        self.bpm_history: List[float] = []
    
    def check_bpm_stability(
        self,
        target_bpm: float,
        tolerance_percent: float = 1.0
    ) -> Dict:
        tolerance_bpm = target_bpm * (tolerance_percent / 100)
        
        results = {
            "is_stable": True,
            "deviations": [],
            "max_deviation": 0.0,
            "recommendation": None
        }
        
        for recorded_bpm in self.bpm_history:
            deviation = abs(recorded_bpm - target_bpm)
            if deviation > tolerance_bpm:
                results["is_stable"] = False
                results["deviations"].append({
                    "recorded": recorded_bpm,
                    "deviation": deviation
                })
                results["max_deviation"] = max(results["max_deviation"], deviation)
        
        if not results["is_stable"]:
            results["recommendation"] = "Enable Master Tempo or re-warp clips"
        
        return results


class SyncEngine:
    """T030, T031: Sync Engine Bridge - Programmatic BPM lock."""
    
    def __init__(self):
        self.master_bpm: float = 128.0
        self.locked_tracks: Set[str] = set()
        self.nudge_amount: float = 0.1
    
    def set_master_bpm(self, bpm: float):
        self.master_bpm = bpm
        logger.info(f"[SYNC] Master BPM set to {bpm}")
    
    def lock_track_bpm(self, track_id: str, source_bpm: Optional[float] = None) -> Dict:
        if source_bpm and source_bpm != self.master_bpm:
            stretch_ratio = self.master_bpm / source_bpm
            warp_required = abs(stretch_ratio - 1.0) > 0.01
        else:
            stretch_ratio = 1.0
            warp_required = False
        
        self.locked_tracks.add(track_id)
        
        return {
            "track_id": track_id,
            "master_bpm": self.master_bpm,
            "source_bpm": source_bpm,
            "stretch_ratio": stretch_ratio,
            "warp_required": warp_required,
            "status": "locked"
        }
    
    def nudge_bpm(self, direction: str, amount: Optional[float] = None) -> float:
        amount = amount or self.nudge_amount
        if direction == "up":
            self.master_bpm += amount
        elif direction == "down":
            self.master_bpm -= amount
        logger.info(f"[SYNC] BPM nudged {direction} to {self.master_bpm:.2f}")
        return self.master_bpm
    
    def generate_bpm_lock_command(self, track_id: str) -> Dict:
        return {
            "action": "set_tempo",
            "track_id": track_id,
            "bpm": self.master_bpm,
            "warp_mode": WarpMode.BEATS.value,
            "warp_enabled": True
        }


# ============================================================================
# T032, T033: GROOVE POOL & GROOVE APPLICATION
# ============================================================================

@dataclass
class GrooveTemplate:
    """Represents extracted groove/swing pattern."""
    name: str
    base_bpm: float
    timing_offsets: List[float]
    velocity_pattern: List[int]
    quantization: int = 16
    intensity: float = 0.5


class GrooveExtractor:
    """T032: Groove Pool Extraction - Extract swing from audio/MIDI clips."""
    
    def __init__(self):
        self.templates: Dict[str, GrooveTemplate] = {}
    
    def extract_from_midi_notes(
        self,
        notes: List[Dict],
        bpm: float,
        template_name: str = "extracted"
    ) -> GrooveTemplate:
        if not notes:
            return GrooveTemplate(
                name=template_name,
                base_bpm=bpm,
                timing_offsets=[],
                velocity_pattern=[]
            )
        
        timing_offsets = []
        velocity_pattern = []
        
        for note in notes:
            beat = note.get("start_beat", 0)
            velocity = note.get("velocity", 100)
            
            grid_16th = round(beat * 4)
            expected_beat = grid_16th / 4
            
            offset_beats = beat - expected_beat
            offset_ms = offset_beats * (60000.0 / bpm)
            
            timing_offsets.append(offset_ms)
            velocity_pattern.append(velocity)
        
        max_offset = max(abs(o) for o in timing_offsets) if timing_offsets else 0
        intensity = min(1.0, max_offset / 20.0)
        
        template = GrooveTemplate(
            name=template_name,
            base_bpm=bpm,
            timing_offsets=timing_offsets,
            velocity_pattern=velocity_pattern,
            quantization=16,
            intensity=intensity
        )
        
        self.templates[template_name] = template
        logger.info(f"[GROOVE] Extracted '{template_name}': {intensity:.2f} intensity")
        return template
    
    def get_template(self, name: str) -> Optional[GrooveTemplate]:
        return self.templates.get(name)


class GrooveApplicator:
    """T033: Groove Application - Apply swing mathematically to notes."""
    
    def apply_groove(
        self,
        notes: List[Dict],
        groove_template: GrooveTemplate,
        intensity_scale: float = 1.0
    ) -> List[Dict]:
        if not notes or not groove_template.timing_offsets:
            return notes
        
        modified_notes = []
        bpm = groove_template.base_bpm
        ms_per_beat = 60000.0 / bpm
        
        for i, note in enumerate(notes):
            new_note = note.copy()
            
            if i < len(groove_template.timing_offsets):
                offset_ms = groove_template.timing_offsets[i]
                offset_ms *= intensity_scale * groove_template.intensity
                offset_beats = offset_ms / ms_per_beat
                new_note["start_beat"] = note["start_beat"] + offset_beats
            
            if i < len(groove_template.velocity_pattern):
                base_velocity = groove_template.velocity_pattern[i]
                new_note["velocity"] = int(
                    note["velocity"] * 0.7 + base_velocity * 0.3 * intensity_scale
                )
            
            modified_notes.append(new_note)
        
        return modified_notes
    
    def generate_swing_groove(self, bpm: float, swing_percent: float = 50) -> GrooveTemplate:
        timing_offsets = []
        for i in range(32):
            if i % 2 == 1:
                offset_ms = (swing_percent - 50) / 50 * 15
                timing_offsets.append(offset_ms)
            else:
                timing_offsets.append(0)
        
        return GrooveTemplate(
            name=f"swing_{swing_percent}pct",
            base_bpm=bpm,
            timing_offsets=timing_offsets,
            velocity_pattern=[100, 90] * 16,
            quantization=16,
            intensity=swing_percent / 100.0
        )


# ============================================================================
# T034, T035, T036: PHRASE MATCHING & STRUCTURAL ALIGNMENT
# ============================================================================

class PhraseMatcher:
    """
    T034: Phrase Matching Analysis - 16/32-bar structure detection.
    T035: Intro/Outro Alignment - 16-bar overlay calculations.
    T036: Modulation Transition - 4-bar key bridge.
    """
    
    BAR_LENGTH = 4.0
    
    def __init__(self):
        self.phrase_boundaries: List[float] = []
        self.modulation_bridges: List[Dict] = []
    
    def find_phrase_boundaries(
        self,
        total_beats: float,
        phrase_length_bars: int = 16
    ) -> List[float]:
        phrase_length = phrase_length_bars * self.BAR_LENGTH
        
        boundaries = []
        current = 0.0
        
        while current < total_beats:
            boundaries.append(current)
            current += phrase_length
        
        self.phrase_boundaries = boundaries
        return boundaries
    
    def calculate_overlay_points(
        self,
        track1_length: float,
        track2_length: float,
        phrase_bars: int = 16
    ) -> List[Dict]:
        phrase_beats = phrase_bars * self.BAR_LENGTH
        
        overlays = []
        track1_outro_start = max(0, track1_length - phrase_beats)
        track2_intro_end = min(track2_length, phrase_beats)
        
        overlays.append({
            "type": "outro_intro_overlay",
            "track1_region": (track1_outro_start, track1_length),
            "track2_region": (0, track2_intro_end),
            "duration_beats": phrase_beats,
            "mix_point": track1_outro_start,
            "energy_curve": "crossfade"
        })
        
        return overlays
    
    def plan_modulation_transition(
        self,
        from_key: str,
        to_key: str,
        start_beat: float,
        bridge_length_bars: int = 4
    ) -> Dict:
        bridge_beats = bridge_length_bars * self.BAR_LENGTH
        
        shifter = PitchShifter()
        path = shifter.find_best_modulation_path(from_key, to_key, max_steps=2)
        
        transition = {
            "start_beat": start_beat,
            "end_beat": start_beat + bridge_beats,
            "duration_bars": bridge_length_bars,
            "from_key": from_key,
            "to_key": to_key,
            "bridge_key": path[0]["key"] if path else from_key,
            "steps": path,
            "automation": []
        }
        
        if len(path) == 1:
            shift = path[0]["shift_from_prev"]
            if shift is not None:
                transition["automation"].append({
                    "type": "pitch_bend",
                    "start": start_beat,
                    "end": start_beat + bridge_beats,
                    "value_start": 0,
                    "value_end": shift * 100
                })
        
        self.modulation_bridges.append(transition)
        return transition
    
    def align_double_drop(
        self,
        track1_phrase_start: float,
        track2_phrase_start: float,
        phrase_bars: int = 32
    ) -> Dict:
        offset = track1_phrase_start - track2_phrase_start
        
        return {
            "track1_phrase_start": track1_phrase_start,
            "track2_phrase_start": track2_phrase_start,
            "offset_beats": offset,
            "offset_ms": offset * (60000.0 / 128),
            "alignment_action": "nudge_track2" if offset > 0 else "nudge_track1",
            "phrase_length_beats": phrase_bars * self.BAR_LENGTH
        }


# ============================================================================
# T037: KEY-LOCK TOGGLE
# ============================================================================

class KeyLockMode(Enum):
    """Key lock operational modes."""
    PITCH_LOCK = "pitch_lock"
    TEMPO_LOCK = "tempo_lock"
    OFF = "off"


class KeyLockController:
    """T037: Key-Lock Toggle - Control pitch_shift vs tempo preservation."""
    
    def __init__(self):
        self.mode: KeyLockMode = KeyLockMode.OFF
        self.original_pitch: Optional[float] = None
        self.original_tempo: Optional[float] = None
    
    def set_mode(self, mode: KeyLockMode, current_pitch: float, current_tempo: float):
        self.mode = mode
        self.original_pitch = current_pitch
        self.original_tempo = current_tempo
        logger.info(f"[KEY-LOCK] Mode set to {mode.value}")
    
    def apply_bpm_change(
        self,
        new_bpm: float,
        current_warp_mode: WarpMode
    ) -> Dict:
        if self.mode == KeyLockMode.PITCH_LOCK:
            return {
                "warp_mode": WarpMode.COMPLEX_PRO,
                "formant_preserve": True,
                "pitch_shift": 0,
                "description": "Pitch locked - using Complex Pro"
            }
        
        elif self.mode == KeyLockMode.TEMPO_LOCK:
            if self.original_tempo:
                ratio = self.original_tempo / new_bpm
                semitones = 12 * math.log2(ratio)
                return {
                    "warp_mode": current_warp_mode,
                    "pitch_shift": semitones,
                    "description": f"Tempo locked - pitch shifted {semitones:.2f} semitones"
                }
        
        return {
            "warp_mode": current_warp_mode,
            "pitch_shift": 0,
            "description": "Key lock off - free adjustment"
        }


# ============================================================================
# T038: CAMELOT WHEEL DISPLAY
# ============================================================================

class CamelotDisplay:
    """T038: Camelot Wheel Display - Logging and visualization utilities."""
    
    @staticmethod
    def format_wheel_ascii(current_key: str, compatible_keys: List[str]) -> str:
        code = CamelotWheel.get_camelot_code(current_key) or "??"
        
        lines = [
            "+--------------------------------------+",
            f"|     CAMELOT WHEEL: {code:^6}           |",
            "+--------------------------------------+",
            f"|  Current: {current_key:^12}               |",
            "+--------------------------------------+",
            "|  Compatible keys:                    |"
        ]
        
        for key in compatible_keys[:6]:
            key_name = CamelotWheel.get_key_from_camelot(key) or "??"
            lines.append(f"|    - {key} ({key_name:^3})                    |")
        
        lines.append("+--------------------------------------+")
        return "\n".join(lines)
    
    @staticmethod
    def log_harmonic_analysis(
        track_id: str,
        key: str,
        bpm: float,
        energy_level: int
    ):
        code = CamelotWheel.get_camelot_code(key) or "??"
        bar = "#" * energy_level + "-" * (10 - energy_level)
        
        logger.info("=" * 50)
        logger.info(f"[HARMONIC ANALYSIS] Track: {track_id}")
        logger.info(f"  Key: {key} -> Camelot {code}")
        logger.info(f"  BPM: {bpm:.2f}")
        logger.info(f"  Energy: {bar} {energy_level}/10")
        logger.info("=" * 50)
        logger.info(f"[HARMONIC ANALYSIS] Track: {track_id}")
        logger.info(f"  Key: {key} → Camelot {code}")
        logger.info(f"  BPM: {bpm:.2f}")
        logger.info(f"  Energy: {bar} {energy_level}/10")
        logger.info("═" * 50)
    
    @staticmethod
    def log_transition_analysis(from_key: str, to_key: str, compatibility_score: float):
        code1 = CamelotWheel.get_camelot_code(from_key) or "??"
        code2 = CamelotWheel.get_camelot_code(to_key) or "??"
        
        if compatibility_score > 0.7:
            status = "[OK] COMPATIBLE"
        elif compatibility_score > 0.4:
            status = "[!] CAUTION"
        else:
            status = "[X] CLASH"
        
        logger.info(f"[TRANSITION] {from_key}({code1}) -> {to_key}({code2})")
        logger.info(f"  Score: {compatibility_score:.2%} | Status: {status}")


# ============================================================================
# T039: AUTO-FIX CLASHING BASELINES
# ============================================================================

class ClashAutoFixer:
    """T039: Auto-Fix Clashing Baselines - Automatically mute weaker track."""
    
    def __init__(self, energy_index: EnergyLevelIndex):
        self.energy_index = energy_index
        self.auto_fixes_applied: List[Dict] = []
    
    def detect_and_fix_clash(
        self,
        track1_id: str,
        track1_key: str,
        track2_id: str,
        track2_key: str,
        active_region: Tuple[float, float]
    ) -> Optional[Dict]:
        distance = CamelotWheel.calculate_distance(track1_key, track2_key)
        
        if distance < 6:
            return None
        
        weaker_track = self.energy_index.get_weaker_track(track1_id, track2_id)
        
        fix_action = {
            "type": "auto_mute",
            "reason": f"Key clash (distance={distance})",
            "track_muted": weaker_track,
            "track_kept": track2_id if weaker_track == track1_id else track1_id,
            "region": active_region,
            "keys": {
                track1_id: track1_key,
                track2_id: track2_key
            }
        }
        
        self.auto_fixes_applied.append(fix_action)
        
        logger.warning(f"[AUTO-FIX] Key clash: {track1_key} vs {track2_key}")
        logger.warning(f"[AUTO-FIX] Muting {weaker_track} (lower energy)")
        
        return fix_action


# ============================================================================
# T040: INTEGRATION TEST - 5-TRACK HARMONIC MINI-MIX
# ============================================================================

class HarmonicMixIntegrationTest:
    """T040: Integration Test - 5-track harmonic mini-mix."""
    
    def __init__(self):
        self.camelot = CamelotWheel()
        self.key_router = KeyRouter()
        self.energy_index = EnergyLevelIndex()
        self.sync_engine = SyncEngine()
        self.groove_extractor = GrooveExtractor()
        self.phrase_matcher = PhraseMatcher()
        self.clash_fixer = ClashAutoFixer(self.energy_index)
        self.display = CamelotDisplay()
    
    def run_5track_mini_mix_test(self) -> Dict:
        """Run integration test with 5 tracks showing harmonic mixing."""
        logger.info("\n" + "=" * 60)
        logger.info("T040: 5-TRACK HARMONIC MINI-MIX INTEGRATION TEST")
        logger.info("=" * 60)
        
        tracks = [
            {"id": "intro_ambient", "key": "Am", "bpm": 125, "energy": 3, "bars": 16},
            {"id": "build_perc", "key": "Am", "bpm": 125, "energy": 6, "bars": 16},
            {"id": "drop_a", "key": "Em", "bpm": 125, "energy": 9, "bars": 32},
            {"id": "break_down", "key": "Dm", "bpm": 125, "energy": 2, "bars": 16},
            {"id": "drop_b", "key": "Am", "bpm": 125, "energy": 10, "bars": 32}
        ]
        
        results = {
            "tracks_analyzed": [],
            "transitions_planned": [],
            "clash_fixes": [],
            "phrase_alignment": None
        }
        
        # Register tracks
        for track in tracks:
            self.energy_index.set_track_energy(track["id"], track["energy"])
            compatible = self.key_router.register_track_key(track["id"], track["key"])
            self.sync_engine.lock_track_bpm(track["id"], track["bpm"])
            
            self.display.log_harmonic_analysis(
                track["id"], track["key"], track["bpm"], track["energy"]
            )
            
            results["tracks_analyzed"].append({
                "id": track["id"],
                "camelot": CamelotWheel.get_camelot_code(track["key"]),
                "compatible": compatible
            })
        
        # Calculate transitions
        logger.info("\n--- TRANSITION ANALYSIS ---")
        recommendations = self.key_router.get_harmonic_mix_recommendations()
        
        for rec in recommendations:
            self.display.log_transition_analysis(
                rec["from_key"], rec["to_key"],
                1.0 if rec["compatible"] else 0.3
            )
            results["transitions_planned"].append(rec)
        
        # Check for clashes
        logger.info("\n--- CLASH DETECTION ---")
        for i in range(len(tracks) - 1):
            fix = self.clash_fixer.detect_and_fix_clash(
                tracks[i]["id"], tracks[i]["key"],
                tracks[i+1]["id"], tracks[i+1]["key"],
                (0, tracks[i]["bars"] * 4)
            )
            if fix:
                results["clash_fixes"].append(fix)
        
        # Calculate phrase alignment
        logger.info("\n--- DOUBLE DROP ALIGNMENT ---")
        drop_a_start = (16 + 16) * 4
        drop_b_start = drop_a_start + (32 * 4) + (16 * 4)
        
        alignment = self.phrase_matcher.align_double_drop(
            drop_a_start, drop_b_start, phrase_bars=32
        )
        results["phrase_alignment"] = alignment
        
        logger.info(f"Drop A start: bar {drop_a_start/4:.0f}")
        logger.info(f"Drop B start: bar {drop_b_start/4:.0f}")
        logger.info(f"Alignment offset: {alignment['offset_beats']:.1f} beats")
        
        # Display Camelot wheel
        logger.info("\n--- CAMELOT WHEEL POSITION ---")
        final_key = tracks[-1]["key"]
        compatible = CamelotWheel.get_compatible_keys(final_key)
        wheel_display = self.display.format_wheel_ascii(final_key, compatible)
        logger.info("\n" + wheel_display)
        
        logger.info("\n" + "=" * 60)
        logger.info("T040: INTEGRATION TEST COMPLETED")
        logger.info("=" * 60)
        
        return results


# ============================================================================
# HARMONIC ENGINE MAIN CLASS
# ============================================================================

class HarmonicEngine:
    """Main Harmonic Engine integrating all T021-T040 features."""
    
    def __init__(self):
        self.camelot = CamelotWheel()
        self.key_detector = KeyDetector()
        self.key_router = KeyRouter()
        self.energy_index = EnergyLevelIndex()
        self.warp_strategy = WarpStrategy()
        self.pitch_shifter = PitchShifter()
        self.rhythm_checker = RhythmConsistencyChecker()
        self.sync_engine = SyncEngine()
        self.groove_extractor = GrooveExtractor()
        self.groove_applicator = GrooveApplicator()
        self.phrase_matcher = PhraseMatcher()
        self.key_lock = KeyLockController()
        self.display = CamelotDisplay()
        self.clash_fixer = ClashAutoFixer(self.energy_index)
    
    def analyze_track(
        self,
        track_id: str,
        audio_features: Optional[Dict] = None,
        filename: Optional[str] = None,
        declared_key: Optional[str] = None,
        declared_bpm: Optional[float] = None
    ) -> Dict:
        result = {
            "track_id": track_id,
            "detected_key": None,
            "detected_bpm": None,
            "camelot_code": None,
            "energy_level": None,
            "confidence": 0.0,
            "sources": []
        }
        
        if declared_key:
            result["detected_key"] = declared_key
            result["sources"].append("metadata")
            result["confidence"] = 0.9
        elif filename:
            key_from_filename = self.key_detector.estimate_key_from_filename(filename)
            if key_from_filename:
                result["detected_key"] = key_from_filename
                result["sources"].append("filename")
                result["confidence"] = 0.7
        
        if not result["detected_key"] and audio_features:
            spectral_key = self.key_detector.detect_key_from_spectral_features(
                audio_features.get("spectral_centroid", 1000),
                audio_features.get("chroma_energy")
            )
            if spectral_key:
                result["detected_key"] = spectral_key
                result["sources"].append("spectral")
                result["confidence"] = 0.5
        
        if declared_bpm:
            result["detected_bpm"] = declared_bpm
        
        if result["detected_key"]:
            result["camelot_code"] = CamelotWheel.get_camelot_code(result["detected_key"])
            self.key_router.register_track_key(track_id, result["detected_key"])
        
        if audio_features:
            result["energy_level"] = self.energy_index.estimate_energy_from_features(
                audio_features.get("spectral_centroid", 1000),
                audio_features.get("rms_energy", -20),
                audio_features.get("transients_per_bar", 8)
            )
            self.energy_index.set_track_energy(track_id, result["energy_level"])
        
        return result
    
    def plan_harmonic_mix(
        self,
        source_track: str,
        target_track: str,
        mix_type: str = "blend"
    ) -> Dict:
        """Plan a harmonic mix between two tracks."""
        source_key = self.key_router.track_keys.get(source_track)
        target_key = self.key_router.track_keys.get(target_track)
        
        if not source_key or not target_key:
            return {"error": "Keys not registered for both tracks"}
        
        plan = {
            "source_track": source_track,
            "target_track": target_track,
            "source_key": source_key,
            "target_key": target_key,
            "mix_type": mix_type,
            "compatible": CamelotWheel.is_compatible(source_key, target_key),
            "distance": CamelotWheel.calculate_distance(source_key, target_key),
            "actions": []
        }
        
        if not plan["compatible"]:
            shift_options = self.pitch_shifter.get_harmonic_shift_options(source_key)
            plan["pitch_shift_options"] = shift_options[:3]
            plan["actions"].append("pitch_shift")
        
        if plan["distance"] >= 4:
            bridge = self.phrase_matcher.plan_modulation_transition(
                source_key, target_key, 0, bridge_length_bars=4
            )
            plan["modulation_bridge"] = bridge
            plan["actions"].append("modulation_bridge")
        
        return plan
    
    def run_integration_test(self) -> Dict:
        test = HarmonicMixIntegrationTest()
        return test.run_5track_mini_mix_test()


# ============================================================================
# PUBLIC API FUNCTIONS
# ============================================================================

def get_camelot_code(key: str) -> Optional[str]:
    return CamelotWheel.get_camelot_code(key)


def get_compatible_keys(key: str) -> List[str]:
    return CamelotWheel.get_compatible_keys(key)


def calculate_key_distance(key1: str, key2: str) -> int:
    return CamelotWheel.calculate_distance(key1, key2)


def is_harmonic_compatible(key1: str, key2: str) -> bool:
    return CamelotWheel.is_compatible(key1, key2)


def calculate_pitch_shift(source_key: str, target_key: str) -> Optional[int]:
    return PitchShifter.calculate_shift(source_key, target_key)


def get_warp_settings(content_type: str) -> Optional[Dict]:
    return WarpStrategy.get_warp_settings(content_type)


def auto_detect_content_type(
    spectral_centroid: float,
    transient_density: float,
    harmonic_ratio: float
) -> str:
    return WarpStrategy.auto_detect_content_type(
        spectral_centroid, transient_density, harmonic_ratio
    )


def extract_groove_from_notes(notes: List[Dict], bpm: float, name: str = "extracted") -> GrooveTemplate:
    extractor = GrooveExtractor()
    return extractor.extract_from_midi_notes(notes, bpm, name)


def apply_groove_to_notes(
    notes: List[Dict],
    groove: GrooveTemplate,
    intensity: float = 1.0
) -> List[Dict]:
    applicator = GrooveApplicator()
    return applicator.apply_groove(notes, groove, intensity)


def generate_swing_groove(bpm: float, swing_percent: float = 50) -> GrooveTemplate:
    applicator = GrooveApplicator()
    return applicator.generate_swing_groove(bpm, swing_percent)


def analyze_harmonic_compatibility(tracks: List[Dict]) -> Dict:
    engine = HarmonicEngine()
    
    for track in tracks:
        engine.key_router.register_track_key(track["id"], track["key"])
    
    recommendations = engine.key_router.get_harmonic_mix_recommendations()
    
    return {
        "tracks_registered": len(tracks),
        "recommendations": recommendations,
        "conflicts": engine.key_router.key_conflicts,
        "compatible_pairs": [
            rec for rec in recommendations if rec["compatible"]
        ]
    }


def find_best_harmonic_path(keys: List[str]) -> List[Dict]:
    if len(keys) <= 1:
        return [{"key": k, "position": i} for i, k in enumerate(keys)]
    
    remaining = keys.copy()
    path = []
    
    current = remaining.pop(0)
    path.append({"key": current, "original_index": 0, "position": 0})
    
    position = 1
    while remaining:
        best_next = None
        best_distance = float('inf')
        best_idx = -1
        
        for i, key in enumerate(remaining):
            dist = CamelotWheel.calculate_distance(current, key)
            if dist < best_distance:
                best_distance = dist
                best_next = key
                best_idx = i
        
        if best_next:
            path.append({
                "key": best_next,
                "distance_from_prev": best_distance,
                "position": position
            })
            current = best_next
            remaining.pop(best_idx)
            position += 1
    
    return path


def run_arc2_integration_test() -> Dict:
    engine = HarmonicEngine()
    return engine.run_integration_test()


# ============================================================================
# MAIN - Self-test
# ============================================================================

if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    
    print("=" * 60)
    print("HARMONIC ENGINE - ARC 2 (T021-T040) SELF-TEST")
    print("=" * 60)
    
    # T021: Test Camelot Wheel
    print("\n[T021] Camelot Wheel Integration:")
    for key in ["Am", "C", "F#m", "G"]:
        code = CamelotWheel.get_camelot_code(key)
        compatible = CamelotWheel.get_compatible_keys(key)
        print(f"  {key} -> {code} | Compatible: {compatible[:4]}")
    
    # T022: Test Key Detection
    print("\n[T022] Key Detection Fallback:")
    detector = KeyDetector()
    test_filename = "Loop_Am_128bpm.wav"
    detected = detector.estimate_key_from_filename(test_filename)
    print(f"  Filename '{test_filename}' -> Detected: {detected}")
    
    # T024: Test Energy Index
    print("\n[T024] Energy Level Indexing:")
    energy = EnergyLevelIndex()
    test_energy = energy.estimate_energy_from_features(2000, -12, 12)
    print(f"  Features (centroid=2000, rms=-12, transients=12) -> Energy: {test_energy}/10")
    
    # T027-T028: Test Pitch Shifting
    print("\n[T027-T028] Pitch Shifting & Harmonic Rules:")
    shift = PitchShifter.calculate_shift("Am", "Em")
    print(f"  Am -> Em: {shift} semitones")
    options = PitchShifter.get_harmonic_shift_options("Am")
    print(f"  Harmonic options from Am: {len(options)} found")
    
    # T032-T033: Test Groove
    print("\n[T032-T033] Groove Extraction & Application:")
    test_notes = [
        {"start_beat": 0, "velocity": 100},
        {"start_beat": 1.02, "velocity": 90},
        {"start_beat": 2, "velocity": 100},
        {"start_beat": 3.03, "velocity": 85}
    ]
    extractor = GrooveExtractor()
    groove = extractor.extract_from_midi_notes(test_notes, 125, "test_groove")
    print(f"  Extracted groove intensity: {groove.intensity:.2f}")
    
    # T040: Run integration test
    print("\n[T040] Running Integration Test...")
    engine = HarmonicEngine()
    results = engine.run_integration_test()
    
    print("\n" + "=" * 60)
    print("SELF-TEST COMPLETED SUCCESSFULLY")
    print("=" * 60)
    print(f"Tracks analyzed: {len(results['tracks_analyzed'])}")
    print(f"Transitions planned: {len(results['transitions_planned'])}")
    print(f"Clash fixes applied: {len(results['clash_fixes'])}")
