"""Audio activity signal provider.

Detect sustained audio activity using existing RmsLevelProvider.
"""

from __future__ import annotations

import threading
import time
from collections import deque
from dataclasses import dataclass
from typing import TYPE_CHECKING

from noteflow.domain.triggers.entities import TriggerSignal, TriggerSource
from noteflow.infrastructure.logging import get_logger

if TYPE_CHECKING:
    import numpy as np
    from numpy.typing import NDArray

    from noteflow.infrastructure.audio import RmsLevelProvider

logger = get_logger(__name__)


@dataclass
class AudioActivitySettings:
    """Configuration for audio activity detection.

    Attributes:
        enabled: Whether audio activity detection is enabled.
        threshold_db: Minimum dB level to consider as activity (default -40 dB).
        window_seconds: Time window for sustained activity detection.
        min_active_ratio: Minimum ratio of active samples in window (0.0-1.0).
        min_samples: Minimum samples required before evaluation.
        max_history: Maximum samples retained in history.
        weight: Confidence weight contributed by this provider.
    """

    enabled: bool
    threshold_db: float
    window_seconds: float
    min_active_ratio: float
    min_samples: int
    max_history: int
    weight: float

    def __post_init__(self) -> None:
        if self.min_samples > self.max_history:
            msg = "min_samples must be <= max_history"
            raise ValueError(msg)


class AudioActivityProvider:
    """Detect sustained audio activity using existing RmsLevelProvider.

    Reuses RmsLevelProvider from infrastructure/audio for dB calculation.
    Tracks activity history over a sliding window and generates signals
    when sustained speech activity is detected.
    """

    def __init__(
        self,
        level_provider: RmsLevelProvider,
        settings: AudioActivitySettings,
    ) -> None:
        """Initialize audio activity provider.

        Args:
            level_provider: Existing RmsLevelProvider instance to reuse.
            settings: Configuration settings for audio activity detection.
        """
        self._level_provider = level_provider
        self._settings = settings
        self._history: deque[tuple[float, bool]] = deque(maxlen=self._settings.max_history)
        self._lock = threading.Lock()
        self._last_signal_state: bool = False
        self._frame_count: int = 0
        self._active_frame_count: int = 0

        logger.info(
            "Audio activity provider initialized",
            enabled=settings.enabled,
            threshold_db=settings.threshold_db,
            window_seconds=settings.window_seconds,
            min_active_ratio=settings.min_active_ratio,
            min_samples=settings.min_samples,
            max_history=settings.max_history,
            weight=settings.weight,
        )

    @property
    def source(self) -> TriggerSource:
        """Get the source type for this provider."""
        return TriggerSource.AUDIO_ACTIVITY

    @property
    def max_weight(self) -> float:
        """Get the maximum weight this provider can contribute."""
        return self._settings.weight

    def update(self, frames: NDArray[np.float32], timestamp: float) -> None:
        """Update activity history with new audio frames.

        Call this from the audio capture callback to feed new samples.

        Args:
            frames: Audio samples as float32 array.
            timestamp: Monotonic timestamp of the audio chunk.
        """
        if not self._settings.enabled:
            return

        db = self._level_provider.get_db(frames)
        is_active = db >= self._settings.threshold_db
        with self._lock:
            self._history.append((timestamp, is_active))
            self._frame_count += 1
            if is_active:
                self._active_frame_count += 1

            # Log summary every 100 frames to avoid spam
            if self._frame_count % 100 == 0:
                logger.debug(
                    "Audio activity update summary",
                    frame_count=self._frame_count,
                    active_frames=self._active_frame_count,
                    history_size=len(self._history),
                    last_db=round(db, 1),
                    last_active=is_active,
                )

    def get_signal(self) -> TriggerSignal | None:
        """Get current signal if sustained activity detected.

        Returns:
            TriggerSignal if activity ratio exceeds threshold, None otherwise.
        """
        if not self._settings.enabled:
            return None

        # Need minimum samples before we can evaluate
        with self._lock:
            history = list(self._history)

        if len(history) < self._settings.min_samples:
            logger.debug(
                "Insufficient samples for signal evaluation",
                history_size=len(history),
                min_samples=self._settings.min_samples,
            )
            return None

        # Prune old samples outside window
        now = time.monotonic()
        cutoff = now - self._settings.window_seconds
        recent = [(ts, active) for ts, active in history if ts >= cutoff]
        pruned_count = len(history) - len(recent)

        if pruned_count > 0:
            logger.debug(
                "Pruned old samples from history",
                pruned_count=pruned_count,
                remaining_count=len(recent),
                window_seconds=self._settings.window_seconds,
            )

        if len(recent) < self._settings.min_samples:
            logger.debug(
                "Insufficient recent samples after pruning",
                recent_count=len(recent),
                min_samples=self._settings.min_samples,
            )
            return None

        # Calculate activity ratio
        active_count = sum(bool(active) for _, active in recent)
        ratio = active_count / len(recent)
        signal_detected = ratio >= self._settings.min_active_ratio

        # Log state transitions (signal detected vs not)
        if signal_detected != self._last_signal_state:
            if signal_detected:
                logger.info(
                    "Audio activity signal detected",
                    activity_ratio=round(ratio, 3),
                    min_active_ratio=self._settings.min_active_ratio,
                    active_count=active_count,
                    sample_count=len(recent),
                    weight=self.max_weight,
                )
            else:
                logger.info(
                    "Audio activity signal cleared",
                    activity_ratio=round(ratio, 3),
                    min_active_ratio=self._settings.min_active_ratio,
                    active_count=active_count,
                    sample_count=len(recent),
                )
            self._last_signal_state = signal_detected

        if not signal_detected:
            return None

        return TriggerSignal(source=self.source, weight=self.max_weight)

    def is_enabled(self) -> bool:
        """Check if this provider is enabled."""
        return self._settings.enabled

    def clear_history(self) -> None:
        """Clear activity history. Useful when recording starts."""
        with self._lock:
            previous_size = len(self._history)
            self._history.clear()
            self._frame_count = 0
            self._active_frame_count = 0
            self._last_signal_state = False

        logger.debug(
            "Audio activity history cleared",
            previous_size=previous_size,
        )
