"""
LED and Audio Alert Controllers for Collision Prediction Feedback.

This module provides LED and audio control for real-time feedback of collision risk levels.

LED Colors:
- Blue: Idle / Initializing (no prediction yet)
- Green: Low risk (< 50%)
- Orange: Medium risk (50-75%)
- Red: High risk (>= 75%)

Audio Alerts:
- Warning beep (600Hz): Medium risk (50-75%)
- Alarm beep (1000Hz): High risk (>= 75%)

Usage:
    from badas.inference.led import LEDController, AudioAlert, create_led_controller

    # LED control
    led = create_led_controller('auto')  # Auto-detect or use dummy
    led.set_risk(0.75)  # Sets orange
    led.close()

    # Audio alerts
    audio = AudioAlert(backend='auto', cooldown_sec=0.5)
    audio.alert_for_risk(0.85)  # Plays alarm
    audio.close()
"""

import time
import glob
import numpy as np
from abc import ABC, abstractmethod
from typing import Optional

# Try to import serial for LED control
try:
    import serial
    SERIAL_AVAILABLE = True
except ImportError:
    serial = None
    SERIAL_AVAILABLE = False


# =============================================================================
# LED CONTROLLERS
# =============================================================================

class BaseLEDController(ABC):
    """Abstract base class for LED controllers."""

    BLUE = 'b'
    GREEN = 'g'
    ORANGE = 'o'
    RED = 'r'
    OFF = '0'

    # Risk thresholds — aligned with RiskConfig defaults
    THRESHOLD_HIGH = 0.75
    THRESHOLD_MEDIUM = 0.5

    @abstractmethod
    def blue(self) -> None:
        """Set LED to blue (idle/initializing)."""
        pass

    @abstractmethod
    def green(self) -> None:
        """Set LED to green (low risk)."""
        pass

    @abstractmethod
    def orange(self) -> None:
        """Set LED to orange (medium risk)."""
        pass

    @abstractmethod
    def red(self) -> None:
        """Set LED to red (high risk)."""
        pass

    @abstractmethod
    def off(self) -> None:
        """Turn LED off."""
        pass

    @abstractmethod
    def close(self) -> None:
        """Clean up resources."""
        pass

    @property
    @abstractmethod
    def state(self) -> str:
        """Get current LED state."""
        pass

    def set_risk(self, probability: float) -> None:
        """
        Set LED color based on risk probability.

        Args:
            probability: Collision probability (0-1)
        """
        if probability >= self.THRESHOLD_HIGH:
            self.red()
        elif probability >= self.THRESHOLD_MEDIUM:
            self.orange()
        else:
            self.green()

    def set_risk_with_thresholds(
        self,
        probability: float,
        high_threshold: float = 0.75,
        medium_threshold: float = 0.5
    ) -> None:
        """
        Set LED color based on risk probability with custom thresholds.

        Args:
            probability: Collision probability (0-1)
            high_threshold: Threshold for red LED (default 0.75)
            medium_threshold: Threshold for orange LED (default 0.5)
        """
        if probability >= high_threshold:
            self.red()
        elif probability >= medium_threshold:
            self.orange()
        else:
            self.green()


class LEDController(BaseLEDController):
    """
    Physical LED controller with serial communication.

    Uses state caching to minimize serial writes - only sends command
    when state actually changes.

    Args:
        port: Serial port path (e.g., '/dev/ttyUSB0')
        baudrate: Serial baudrate (default 115200)
        init_delay: Delay after serial init in seconds (default 0.1)

    Raises:
        ImportError: If pyserial is not installed
        serial.SerialException: If port cannot be opened
    """

    def __init__(
        self,
        port: str = '/dev/ttyUSB0',
        baudrate: int = 115200,
        init_delay: float = 0.1
    ):
        if not SERIAL_AVAILABLE:
            raise ImportError(
                "pyserial is required for LED control. "
                "Install with: pip install pyserial"
            )

        self._port = port
        self._baudrate = baudrate
        self._ser = serial.Serial(port, baudrate, timeout=0.1)
        self._current_state: Optional[str] = None

        # Allow serial to initialize
        time.sleep(init_delay)

        # Start in idle state
        self.blue()

    def _send(self, cmd: str) -> None:
        """Send command only if state changed (avoids redundant writes)."""
        if cmd != self._current_state:
            self._ser.write(cmd.encode())
            self._current_state = cmd

    def blue(self) -> None:
        self._send(self.BLUE)

    def green(self) -> None:
        self._send(self.GREEN)

    def orange(self) -> None:
        self._send(self.ORANGE)

    def red(self) -> None:
        self._send(self.RED)

    def off(self) -> None:
        self._send(self.OFF)

    def close(self) -> None:
        """Turn off LED and close serial connection."""
        self.off()
        self._ser.close()

    @property
    def state(self) -> str:
        """Get current LED state."""
        return self._current_state or 'unknown'

    @property
    def port(self) -> str:
        """Get serial port path."""
        return self._port

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
        return False


class DummyLEDController(BaseLEDController):
    """
    Simulated LED controller for testing without physical hardware.

    Tracks state internally without any serial communication.
    Useful for:
    - Testing without hardware
    - Development on machines without LED connected
    - Headless operation where LED state is logged elsewhere
    """

    def __init__(self):
        self._current_state: str = 'blue'

    def blue(self) -> None:
        self._current_state = 'blue'

    def green(self) -> None:
        self._current_state = 'green'

    def orange(self) -> None:
        self._current_state = 'orange'

    def red(self) -> None:
        self._current_state = 'red'

    def off(self) -> None:
        self._current_state = 'off'

    def close(self) -> None:
        """No-op for dummy controller."""
        pass

    @property
    def state(self) -> str:
        return self._current_state

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
        return False


def create_led_controller(
    port: Optional[str] = 'auto',
    fallback_to_dummy: bool = True
) -> BaseLEDController:
    """
    Create an appropriate LED controller based on available hardware.

    Args:
        port: Serial port path, 'auto' for auto-detection, or None to disable
        fallback_to_dummy: If True, return DummyLEDController on failure

    Returns:
        LEDController if hardware available, DummyLEDController otherwise

    Raises:
        RuntimeError: If port specified, hardware not found, and fallback disabled
    """
    if port is None:
        return DummyLEDController()

    if port == 'auto':
        # Auto-detect USB serial ports
        usb_ports = glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')
        if usb_ports:
            port = sorted(usb_ports)[0]
        else:
            if fallback_to_dummy:
                return DummyLEDController()
            raise RuntimeError("No USB serial ports found for LED controller")

    try:
        return LEDController(port)
    except Exception as e:
        if fallback_to_dummy:
            return DummyLEDController()
        raise RuntimeError(f"Could not connect to LED on {port}: {e}")


# Color mapping for overlay rendering (BGR for OpenCV)
LED_COLORS_BGR = {
    'blue': (255, 100, 0),
    'green': (0, 255, 0),
    'orange': (0, 165, 255),
    'red': (0, 0, 255),
    'off': (50, 50, 50),
    'unknown': (128, 128, 128)
}

# Color mapping for matplotlib (RGB)
LED_COLORS_RGB = {
    'blue': (0, 100, 255),
    'green': (0, 255, 0),
    'orange': (255, 165, 0),
    'red': (255, 0, 0),
    'off': (50, 50, 50),
    'unknown': (128, 128, 128)
}


# =============================================================================
# AUDIO ALERTS
# =============================================================================

class BaseAudioAlert(ABC):
    """Abstract base class for audio alerts."""

    # Risk thresholds — aligned with RiskConfig defaults
    THRESHOLD_HIGH = 0.75
    THRESHOLD_MEDIUM = 0.5

    @abstractmethod
    def beep(self, frequency: int = 800, duration_ms: int = 150) -> None:
        """Play a beep at specified frequency and duration."""
        pass

    @abstractmethod
    def warning(self) -> None:
        """Play warning sound (medium risk)."""
        pass

    @abstractmethod
    def alarm(self) -> None:
        """Play alarm sound (high risk)."""
        pass

    @abstractmethod
    def close(self) -> None:
        """Clean up resources."""
        pass

    def alert_for_risk(self, probability: float) -> None:
        """Play appropriate alert based on risk level."""
        if probability >= self.THRESHOLD_HIGH:
            self.alarm()
        elif probability >= self.THRESHOLD_MEDIUM:
            self.warning()

    def enable(self) -> None:
        """Enable audio alerts."""
        pass

    def disable(self) -> None:
        """Disable audio alerts."""
        pass


class AudioAlert(BaseAudioAlert):
    """
    Audio alert controller for collision warnings.

    Supports multiple backends:
    - pygame (recommended, best quality)
    - simpleaudio (alternative)
    - bell (system bell, basic fallback)

    Args:
        backend: 'pygame', 'simpleaudio', 'bell', or 'auto'
        cooldown_sec: Minimum time between alerts to prevent spam

    Usage:
        audio = AudioAlert(backend='auto', cooldown_sec=0.5)
        audio.alert_for_risk(0.85)  # Plays alarm
        audio.warning()  # Play warning beep
        audio.close()
    """

    def __init__(self, backend: str = 'auto', cooldown_sec: float = 1.0):
        self.cooldown_sec = cooldown_sec
        self._last_alert_time = 0.0
        self._backend: Optional[str] = None
        self._enabled = True
        self._pygame = None
        self._simpleaudio = None

        # Try to initialize backend
        if backend == 'auto':
            for b in ['pygame', 'simpleaudio', 'bell']:
                if self._init_backend(b):
                    break
        else:
            self._init_backend(backend)

    def _init_backend(self, backend: str) -> bool:
        """Initialize specified backend."""
        try:
            if backend == 'pygame':
                import pygame
                pygame.mixer.init(frequency=22050, size=-16, channels=1)
                self._backend = 'pygame'
                self._pygame = pygame
                return True
            elif backend == 'simpleaudio':
                import simpleaudio
                self._backend = 'simpleaudio'
                self._simpleaudio = simpleaudio
                return True
            elif backend == 'bell':
                self._backend = 'bell'
                return True
        except Exception:
            pass
        return False

    def _generate_tone(self, frequency: int = 440, duration_ms: int = 200, volume: float = 0.5):
        """Generate a simple sine wave tone."""
        sample_rate = 22050
        n_samples = int(sample_rate * duration_ms / 1000)
        t = np.linspace(0, duration_ms / 1000, n_samples, False)
        tone = np.sin(2 * np.pi * frequency * t) * volume

        # Apply fade in/out to avoid clicks
        fade_samples = min(100, n_samples // 4)
        fade_in = np.linspace(0, 1, fade_samples)
        fade_out = np.linspace(1, 0, fade_samples)
        tone[:fade_samples] *= fade_in
        tone[-fade_samples:] *= fade_out

        return (tone * 32767).astype(np.int16)

    def beep(self, frequency: int = 800, duration_ms: int = 150) -> None:
        """Play a single beep."""
        if not self._enabled or not self._backend:
            return

        # Check cooldown
        now = time.time()
        if now - self._last_alert_time < self.cooldown_sec:
            return
        self._last_alert_time = now

        if self._backend == 'pygame':
            tone = self._generate_tone(frequency, duration_ms)
            sound = self._pygame.sndarray.make_sound(tone)
            sound.play()
        elif self._backend == 'simpleaudio':
            tone = self._generate_tone(frequency, duration_ms)
            self._simpleaudio.play_buffer(tone, 1, 2, 22050)
        elif self._backend == 'bell':
            print('\a', end='', flush=True)

    def warning(self) -> None:
        """Play warning beep (medium risk)."""
        self.beep(frequency=600, duration_ms=100)

    def alarm(self) -> None:
        """Play alarm beep (high risk)."""
        self.beep(frequency=1000, duration_ms=200)

    def enable(self) -> None:
        """Enable audio alerts."""
        self._enabled = True

    def disable(self) -> None:
        """Disable audio alerts."""
        self._enabled = False

    def close(self) -> None:
        """Clean up audio resources."""
        if self._backend == 'pygame' and self._pygame:
            try:
                self._pygame.mixer.quit()
            except Exception:
                pass

    @property
    def backend(self) -> Optional[str]:
        """Get current backend name."""
        return self._backend

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
        return False


class DummyAudioAlert(BaseAudioAlert):
    """
    Simulated audio alert (no sound output).

    Useful for testing or when audio hardware is unavailable.
    Tracks the last alert type for verification.
    """

    def __init__(self):
        self._last_alert: Optional[str] = None

    def beep(self, frequency: int = 800, duration_ms: int = 150) -> None:
        self._last_alert = f'beep({frequency}Hz, {duration_ms}ms)'

    def warning(self) -> None:
        self._last_alert = 'warning'

    def alarm(self) -> None:
        self._last_alert = 'alarm'

    def close(self) -> None:
        pass

    @property
    def last_alert(self) -> Optional[str]:
        """Get the last alert that was triggered."""
        return self._last_alert

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        return False


def create_audio_alert(
    backend: str = 'auto',
    cooldown_sec: float = 1.0,
    fallback_to_dummy: bool = True
) -> BaseAudioAlert:
    """
    Create an appropriate audio alert controller.

    Args:
        backend: 'pygame', 'simpleaudio', 'bell', 'auto', or None
        cooldown_sec: Minimum time between alerts
        fallback_to_dummy: If True, return DummyAudioAlert on failure

    Returns:
        AudioAlert if backend available, DummyAudioAlert otherwise
    """
    if backend is None:
        return DummyAudioAlert()

    try:
        alert = AudioAlert(backend=backend, cooldown_sec=cooldown_sec)
        if alert.backend is not None:
            return alert
    except Exception:
        pass

    if fallback_to_dummy:
        return DummyAudioAlert()

    raise RuntimeError("No audio backend available")
