"""Signal classification - type detection and characterization.

Provides the ``classify_signal`` public API and all supporting helpers for
determining whether a signal is digital, analog, mixed, or DC, and extracting
key characteristics like periodicity and noise level.

References:
    IEEE 181-2011: Standard for Transitional Waveform Definitions
    IEEE 1057-2017: Standard for Digitizing Waveform Recorders
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import numpy as np
from numpy.typing import NDArray

from oscura.inference.signal_intelligence.detection import (
    _count_edges,
    _detect_digital_signal,
    _detect_edge_periodicity,
    _detect_periodicity,
    _detect_periodicity_fft,
    _estimate_noise_level,
)

if TYPE_CHECKING:
    from oscura.core.types import WaveformTrace


def _extract_signal_data(
    trace: WaveformTrace | NDArray[np.floating[Any]],
    sample_rate: float,
) -> tuple[NDArray[np.floating[Any]], float]:
    """Extract signal data and sample rate from trace or array.

    Args:
        trace: Input waveform trace or numpy array.
        sample_rate: Sample rate in Hz (only used if trace is ndarray).

    Returns:
        Tuple of (data_array, effective_sample_rate).
    """
    if isinstance(trace, np.ndarray):
        return trace, sample_rate

    # It's a WaveformTrace
    data = trace.data
    if hasattr(trace, "metadata") and hasattr(trace.metadata, "sample_rate"):
        sample_rate = trace.metadata.sample_rate

    return data, sample_rate


def _create_insufficient_data_result() -> dict[str, Any]:
    """Create classification result for insufficient data.

    Returns:
        Classification dictionary indicating insufficient data.
    """
    return {
        "type": "unknown",
        "signal_type": "unknown",
        "is_digital": False,
        "is_periodic": False,
        "characteristics": ["insufficient_data"],
        "dc_component": False,
        "frequency_estimate": None,
        "snr_db": None,
        "confidence": 0.0,
        "noise_level": 0.0,
        "levels": None,
    }


def _compute_signal_statistics(data: NDArray[np.floating[Any]]) -> dict[str, float]:
    """Compute basic signal statistics needed for classification.

    Args:
        data: Signal data array.

    Returns:
        Dictionary with mean, std, min, max, amplitude statistics.
    """
    return {
        "mean": float(np.mean(data)),
        "std": float(np.std(data)),
        "min": float(np.min(data)),
        "max": float(np.max(data)),
        "amplitude": float(np.max(data) - np.min(data)),
    }


def _is_dc_signal(stats: dict[str, float]) -> bool:
    """Check if signal is essentially DC (constant or nearly constant).

    A signal is considered DC if it has negligible variation. This covers:
    - Perfectly constant signals (amplitude == 0)
    - Signals with tiny noise on a constant level (e.g., 3.3V + 1mV noise)

    Detection strategy:
    1. Absolute std threshold catches near-zero-variation signals
    2. For signals with small but nonzero std, use a noise-envelope test:
       a Gaussian noise signal has amplitude/std ~ 4-6, while a real
       periodic signal has amplitude/std ~ 2-3. We check both the
       relative std (std/|mean|) and the noise-like envelope ratio
       (amplitude/std) to avoid false positives on small periodic signals.

    Args:
        stats: Signal statistics dictionary.

    Returns:
        True if signal appears to be DC.
    """
    std = stats["std"]
    amplitude = stats["amplitude"]
    mean_abs = abs(stats["mean"])

    # Absolute threshold: very small standard deviation
    if std < 1e-6:
        return True

    # Relative threshold: std is tiny compared to the mean value AND
    # the variation looks like noise (amplitude/std > 3.5, typical for
    # Gaussian noise) rather than a structured signal. A pure sine
    # wave has amplitude/std = 2*sqrt(2) ~ 2.83, so 3.5 excludes it.
    if mean_abs > 1e-10 and std / mean_abs < 0.01:
        # Additional check: variation should look noise-like, not
        # periodic. For Gaussian noise, amplitude ~ 4-6*std.
        # For a sine wave, amplitude = 2*sqrt(2)*std ~ 2.83*std.
        if std > 0 and amplitude / std > 3.5:
            return True

    return False


def _create_dc_result(stats: dict[str, float]) -> dict[str, Any]:
    """Create classification result for DC signals.

    Args:
        stats: Signal statistics dictionary.

    Returns:
        Classification dictionary for DC signal.
    """
    return {
        "type": "dc",
        "signal_type": "dc",
        "is_digital": False,
        "is_periodic": False,
        "characteristics": ["dc", "constant"],
        "dc_component": True,
        "frequency_estimate": None,
        "snr_db": None,
        "confidence": 0.95,
        "noise_level": stats["std"],
        "levels": None,
    }


def _add_noise_characteristics(
    characteristics: list[str],
    noise_level: float,
    amplitude: float,
) -> None:
    """Add noise-related characteristics based on noise level.

    Args:
        characteristics: List to append characteristics to.
        noise_level: Estimated RMS noise level.
        amplitude: Signal amplitude.
    """
    if amplitude > 0:
        noise_ratio = noise_level / amplitude
        if noise_ratio < 0.01:
            characteristics.append("clean")
        elif noise_ratio < 0.05:
            characteristics.append("low_noise")
        elif noise_ratio < 0.1:
            characteristics.append("moderate_noise")
        else:
            characteristics.append("noisy")


def _classify_periodicity(
    data: NDArray[np.floating[Any]],
    sample_rate: float,
    threshold: float,
    is_digital: bool,
    digital_levels: dict[str, float] | None,
    n: int,
) -> tuple[bool, float | None, float]:
    """Classify signal periodicity using multiple methods.

    Uses autocorrelation, FFT, and edge-based detection.

    Args:
        data: Signal data array.
        sample_rate: Sampling rate in Hz.
        threshold: Correlation threshold for periodic detection.
        is_digital: Whether signal appears digital.
        digital_levels: Digital levels if applicable.
        n: Number of data points.

    Returns:
        Tuple of (is_periodic, period_seconds, confidence).
    """
    # Try autocorrelation first
    is_periodic, period, confidence = _detect_periodicity(data, sample_rate, threshold)
    if is_periodic:
        # Always reconcile with FFT to catch aliasing issues
        return _reconcile_period_estimates(
            is_periodic, period, confidence, data, sample_rate, threshold
        )

    # Try FFT-based detection
    is_periodic, period, confidence = _detect_periodicity_fft(data, sample_rate)
    if is_periodic:
        return is_periodic, period, confidence

    # Try edge-based detection for digital signals
    if is_digital and digital_levels is not None:
        is_periodic, period, confidence = _detect_edge_periodicity(
            data, sample_rate, digital_levels
        )
        if is_periodic:
            # Reconcile with other estimates
            return _reconcile_period_estimates(
                is_periodic, period, confidence, data, sample_rate, threshold
            )

    return False, None, 0.0


def _reconcile_period_estimates(
    is_periodic: bool,
    period: float | None,
    confidence: float,
    data: NDArray[np.floating[Any]],
    sample_rate: float,
    threshold: float,
) -> tuple[bool, float | None, float]:
    """Reconcile period estimates from different detection methods.

    Args:
        is_periodic: Whether periodic was detected.
        period: Period estimate in seconds.
        confidence: Confidence in the estimate.
        data: Signal data array.
        sample_rate: Sampling rate in Hz.
        threshold: Detection threshold.

    Returns:
        Reconciled tuple of (is_periodic, period, confidence).
    """
    if not is_periodic or period is None:
        return is_periodic, period, confidence

    # Try to confirm with FFT
    fft_periodic, fft_period, fft_conf = _detect_periodicity_fft(data, sample_rate)
    if fft_periodic and fft_period is not None:
        # Check if periods agree (within 20%)
        ratio = period / fft_period
        if 0.8 <= ratio <= 1.2:
            # Periods agree, boost confidence
            return (
                True,
                (period + fft_period) / 2,
                max(confidence, fft_conf),
            )
        elif 1.8 <= ratio <= 2.2 or 0.45 <= ratio <= 0.55:
            # One is likely a harmonic, use FFT estimate
            return True, fft_period, fft_conf
        else:
            # Significant disagreement - FFT is generally more accurate
            # for undersampled signals, so prefer it when confidence is high
            if fft_conf > 0.8:
                return True, fft_period, fft_conf

    return is_periodic, period, confidence


def _add_transient_characteristics(
    characteristics: list[str],
    data: NDArray[np.floating[Any]],
    stats: dict[str, float],
    digital_levels: dict[str, float] | None,
) -> None:
    """Add transient and edge characteristics to the list.

    Args:
        characteristics: List to append characteristics to.
        data: Signal data array.
        stats: Signal statistics.
        digital_levels: Digital levels if applicable.
    """
    # Check for transients (large sudden changes)
    if len(data) > 10:
        diffs = np.abs(np.diff(data))
        max_diff = np.max(diffs)
        median_diff = np.median(diffs)

        if median_diff > 0 and max_diff > 10 * median_diff:
            characteristics.append("transient")


def _detect_mixed_signal(
    data: NDArray[np.floating[Any]],
    digital_levels: dict[str, float],
) -> bool:
    """Detect if a digital signal has analog variation (mixed signal).

    Args:
        data: Signal data array.
        digital_levels: Digital levels dict.

    Returns:
        True if signal shows mixed digital/analog characteristics.
    """
    threshold = (digital_levels["low"] + digital_levels["high"]) / 2
    level_range = digital_levels["high"] - digital_levels["low"]

    # Check variation around each level
    high_samples = data[data > threshold]
    low_samples = data[data <= threshold]

    if len(high_samples) > 10 and len(low_samples) > 10:
        high_std = np.std(high_samples)
        low_std = np.std(low_samples)

        # If variation around levels is more than 10% of level separation,
        # it's likely a mixed signal
        if max(high_std, low_std) > level_range * 0.1:
            return True

    return False


def _compute_snr(amplitude: float, noise_level: float) -> float | None:
    """Compute signal-to-noise ratio for classification.

    Args:
        amplitude: Signal amplitude.
        noise_level: Estimated noise level.

    Returns:
        SNR in dB or None if not calculable.
    """
    if noise_level > 0 and amplitude > 0:
        signal_power = amplitude**2 / 8
        noise_power = noise_level**2
        if noise_power > 1e-20:
            return float(10 * np.log10(signal_power / noise_power))
    return None


def _create_classification_result(
    signal_type: str,
    is_digital: bool,
    is_periodic: bool,
    characteristics: list[str],
    dc_component: bool,
    frequency_estimate: float | None,
    snr_db: float | None,
    confidence: float,
    noise_level: float,
    digital_levels: dict[str, float] | None,
) -> dict[str, Any]:
    """Create the final classification result dictionary.

    Args:
        signal_type: Classified signal type.
        is_digital: Whether signal is digital.
        is_periodic: Whether signal is periodic.
        characteristics: List of characteristics.
        dc_component: Whether DC component is significant.
        frequency_estimate: Estimated frequency in Hz.
        snr_db: Signal-to-noise ratio in dB.
        confidence: Classification confidence.
        noise_level: Estimated noise level.
        digital_levels: Digital levels if applicable.

    Returns:
        Complete classification dictionary.
    """
    return {
        "type": signal_type,
        "signal_type": signal_type,
        "is_digital": is_digital,
        "is_periodic": is_periodic,
        "characteristics": characteristics,
        "dc_component": dc_component,
        "frequency_estimate": frequency_estimate,
        "dominant_frequency": frequency_estimate,
        "snr_db": snr_db,
        "confidence": confidence,
        "noise_level": noise_level,
        "levels": digital_levels,
    }


def _build_characteristics(
    data: NDArray[np.floating[Any]],
    stats: dict[str, float],
    is_digital: bool,
    digital_levels: dict[str, float] | None,
) -> list[str]:
    """Build initial characteristics list.

    Args:
        data: Signal data.
        stats: Signal statistics.
        is_digital: Whether signal is digital.
        digital_levels: Digital levels if applicable.

    Returns:
        List of characteristic strings.
    """
    characteristics: list[str] = []
    if is_digital:
        characteristics.append("digital_levels")

    noise_level = _estimate_noise_level(data)
    _add_noise_characteristics(characteristics, noise_level, stats["amplitude"])
    return characteristics


def _analyze_periodicity(
    data: NDArray[np.floating[Any]],
    trace_sample_rate: float,
    periodicity_threshold: float,
    is_digital: bool,
    digital_levels: dict[str, float] | None,
    n: int,
    characteristics: list[str],
) -> tuple[bool, float | None, float]:
    """Analyze signal periodicity.

    Args:
        data: Signal data.
        trace_sample_rate: Sample rate.
        periodicity_threshold: Detection threshold.
        is_digital: Whether signal is digital.
        digital_levels: Digital levels if applicable.
        n: Data length.
        characteristics: Characteristics list to update.

    Returns:
        Tuple of (is_periodic, frequency_estimate, periodicity_score).
    """
    is_periodic, period_estimate, periodicity_score = _classify_periodicity(
        data,
        trace_sample_rate,
        periodicity_threshold,
        is_digital,
        digital_levels,
        n,
    )

    # Check if detected periodicity is actually just a single event
    # (e.g., single pulse that appears periodic due to autocorrelation artifacts)
    # Only applies to digital signals with very few edges and asymmetric duty cycle
    if is_periodic and period_estimate is not None:
        duration = n / trace_sample_rate
        num_periods = duration / period_estimate

        # For digital signals with < 1.2 periods, check if it's a true periodic
        # signal or just a single pulse/transient
        if num_periods < 1.2 and is_digital and digital_levels is not None:
            edge_count = _count_edges(data, digital_levels)
            # Single pulse has 2 edges; check duty cycle to distinguish
            # from a proper square wave with 1 complete period
            if edge_count <= 3:
                # Calculate duty cycle (proportion of time high)
                threshold = (digital_levels["low"] + digital_levels["high"]) / 2
                high_samples = np.sum(data > threshold)
                duty_cycle = high_samples / n

                # Single pulse typically has very asymmetric duty cycle
                # (< 30% or > 70%), while periodic square wave is ~50%
                if duty_cycle < 0.3 or duty_cycle > 0.7:
                    is_periodic = False
                    period_estimate = None
                    periodicity_score = 0.0

    if is_periodic:
        characteristics.append("periodic")
        frequency_estimate = (
            1.0 / period_estimate if period_estimate and period_estimate > 0 else None
        )
    else:
        characteristics.append("aperiodic")
        frequency_estimate = None

    return is_periodic, frequency_estimate, periodicity_score


def classify_signal(
    trace: WaveformTrace | NDArray[np.floating[Any]],
    sample_rate: float = 1.0,
    *,
    digital_threshold_ratio: float = 0.8,
    dc_threshold_percent: float = 90.0,
    periodicity_threshold: float = 0.7,
) -> dict[str, Any]:
    """Classify signal type and characteristics.

    Automatically detects whether a signal is digital, analog, or mixed,
    identifies key characteristics like periodicity and noise.

    Args:
        trace: Input waveform trace or numpy array to classify.
        sample_rate: Sample rate in Hz (only used if trace is ndarray).
        digital_threshold_ratio: Ratio for digital detection (0-1).
        dc_threshold_percent: Percentage of DC for DC classification.
        periodicity_threshold: Correlation threshold for periodic (0-1).

    Returns:
        Dictionary with signal_type, is_digital, is_periodic,
        characteristics, frequency_estimate, snr_db, confidence,
        noise_level, levels.

    Example:
        >>> info = osc.classify_signal(trace)
        >>> print(f"Type: {info['signal_type']}")

    References:
        IEEE 181-2011: Digital waveform characterization
    """
    data, trace_sample_rate = _extract_signal_data(trace, sample_rate)
    n = len(data)

    if n < 10:
        return _create_insufficient_data_result()

    stats = _compute_signal_statistics(data)

    if _is_dc_signal(stats):
        return _create_dc_result(stats)

    is_digital, digital_levels, confidence = _detect_digital_signal(data, digital_threshold_ratio)
    signal_type = "digital" if is_digital else "analog"

    characteristics = _build_characteristics(data, stats, is_digital, digital_levels)

    is_periodic, frequency_estimate, periodicity_score = _analyze_periodicity(
        data,
        trace_sample_rate,
        periodicity_threshold,
        is_digital,
        digital_levels,
        n,
        characteristics,
    )
    confidence = max(confidence, periodicity_score) if is_periodic else confidence

    dc_component = abs(stats["mean"]) > (stats["amplitude"] * dc_threshold_percent / 100.0)
    _add_transient_characteristics(
        characteristics, data, stats, digital_levels if is_digital else None
    )

    if is_digital and digital_levels and _detect_mixed_signal(data, digital_levels):
        signal_type = "mixed"
        characteristics.append("analog_variation")

    noise_level = _estimate_noise_level(data)
    snr_db = _compute_snr(stats["amplitude"], noise_level)

    return _create_classification_result(
        signal_type,
        is_digital,
        is_periodic,
        characteristics,
        dc_component,
        frequency_estimate,
        snr_db,
        confidence,
        noise_level,
        digital_levels,
    )
