"""Dynamic range analyzer for AI MusiMuse.

This module defines :class:`DynamicAnalyzer`, which extracts dynamic
characteristics from decoded audio using shared DSP utilities.

No rhythm, harmony, tempo, or machine learning is performed.
The analyzer must not perform logging — the pipeline owns all logging.
The analyzer must not access the database — persistence is the
pipeline's responsibility.
"""

from __future__ import annotations

import time

import numpy as np

from analyzer.analyzer import Analyzer
from analyzer.context import AnalysisContext
from analyzer.feature import Feature, FeatureSet
from analyzer.result import AnalysisResult
from core.exceptions import AnalysisError
from dsp.config import RMS_HOP_SIZE, RMS_WINDOW_SIZE
from dsp.dynamics import (
    compute_clipping_ratio,
    compute_dynamic_range,
    compute_headroom,
    compute_rms_envelope,
)


def _dbfs(value: float) -> float:
    """Convert a linear amplitude value to dBFS.

    Args:
        value: Linear amplitude (>= 0).

    Returns:
        dBFS value, or ``-inf`` for zero amplitude.
    """
    if value <= 0.0:
        return float("-inf")
    return float(20.0 * np.log10(value))


class DynamicAnalyzer(Analyzer):
    """Extracts dynamic range features from decoded audio.

    Produces 7 features: crest factor, crest factor dB, dynamic range,
    headroom, clipping ratio, average RMS dB, and RMS variability.

    All calculations use shared DSP utilities from :mod:`dsp.dynamics`
    and configuration from :mod:`dsp.config`.
    """

    name = "dynamic"
    version = "1.0.0"

    def analyze(self, context: AnalysisContext) -> AnalysisResult:
        """Analyze decoded audio and return dynamic range features.

        Args:
            context: The analysis context containing decoded audio.

        Returns:
            An :class:`AnalysisResult` with dynamic feature data.

        Raises:
            AnalysisError: If the audio is empty, contains NaN, or
                contains infinite values.
        """
        start = time.perf_counter()

        audio = context.decoded_audio
        samples = audio.samples

        if samples.size == 0:
            raise AnalysisError("Audio contains no samples")

        if np.any(np.isnan(samples)):
            raise AnalysisError("Audio contains NaN values")

        if np.any(np.isinf(samples)):
            raise AnalysisError("Audio contains infinite values")

        # Convert to mono
        mono = np.mean(samples[:, :2], axis=1) if audio.channels > 1 else samples[:, 0]

        peak = float(np.max(np.abs(mono)))
        rms = float(np.sqrt(np.mean(mono**2)))

        # Crest factor
        crest_factor = peak / rms if rms > 0.0 else 1.0

        crest_factor_db = (
            float(20.0 * np.log10(crest_factor)) if crest_factor > 0.0 else 0.0
        )

        # RMS envelope
        envelope = compute_rms_envelope(mono, RMS_WINDOW_SIZE, RMS_HOP_SIZE)

        # Dynamic range
        dynamic_range = compute_dynamic_range(envelope)

        # Headroom
        headroom = compute_headroom(peak)

        # Clipping ratio
        clipping_ratio = compute_clipping_ratio(mono)

        # Average RMS dB
        average_rms_db = _dbfs(rms)

        # RMS variability (std dev of envelope in dB)
        positive_env = envelope[envelope > 0.0]
        if len(positive_env) > 1:
            env_db = 20.0 * np.log10(positive_env)
            rms_variability = float(np.std(env_db))
        else:
            rms_variability = 0.0

        features: list[Feature] = [
            Feature(
                name="dynamic.crest_factor",
                value=crest_factor,
                unit="ratio",
                analyzer=self.name,
                version=self.version,
            ),
            Feature(
                name="dynamic.crest_factor_db",
                value=crest_factor_db,
                analyzer=self.name,
                version=self.version,
            ),
            Feature(
                name="dynamic.range",
                value=dynamic_range,
                unit="dB",
                analyzer=self.name,
                version=self.version,
            ),
            Feature(
                name="dynamic.headroom",
                value=headroom,
                analyzer=self.name,
                version=self.version,
            ),
            Feature(
                name="dynamic.clipping_ratio",
                value=clipping_ratio,
                analyzer=self.name,
                version=self.version,
            ),
            Feature(
                name="dynamic.average_rms_db",
                value=average_rms_db,
                unit="dBFS",
                analyzer=self.name,
                version=self.version,
            ),
            Feature(
                name="dynamic.rms_variability",
                value=rms_variability,
                unit="dB",
                analyzer=self.name,
                version=self.version,
            ),
        ]

        feature_set = FeatureSet(features)
        elapsed_ms = (time.perf_counter() - start) * 1000.0

        return AnalysisResult(
            analyzer_name=self.name,
            analyzer_version=self.version,
            execution_time_ms=elapsed_ms,
            success=True,
            warnings=(),
            feature_set=feature_set,
        )
