"""Signal detection utilities for digital/periodic signal identification.

Provides low-level detection functions used across the signal intelligence
package. This module has no internal dependencies and serves as the foundation
layer.

References:
    IEEE 181-2011: Standard for Transitional Waveform Definitions
"""

from __future__ import annotations

from typing import Any

import numpy as np
from numpy.typing import NDArray


def _detect_digital_signal(
    data: NDArray[np.floating[Any]],
    threshold_ratio: float,
) -> tuple[bool, dict[str, float] | None, float]:
    """Detect if signal is digital based on bimodal distribution.

    Args:
        data: Signal data array.
        threshold_ratio: Ratio of samples at two levels to consider digital.

    Returns:
        Tuple of (is_digital, levels_dict, confidence).
    """
    # Use histogram to find peaks
    # Use more bins for better resolution on digital signals
    n_bins = min(100, len(np.unique(data)))
    hist, bin_edges = np.histogram(data, bins=n_bins)
    bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2

    # Find peaks (local maxima or significant bins)
    peaks: list[tuple[int, int, float]] = []

    # Special case: if only 2 bins (perfect digital signal), both are peaks
    if len(hist) == 2:
        for i in range(len(hist)):
            if hist[i] > len(data) * 0.01:
                peaks.append((i, hist[i], bin_centers[i]))
    else:
        # Find local maxima in histogram
        for i in range(1, len(hist) - 1):
            if hist[i] > hist[i - 1] and hist[i] > hist[i + 1]:
                # Lower threshold for peak detection
                if hist[i] > len(data) * 0.01:  # At least 1% of samples
                    peaks.append((i, hist[i], bin_centers[i]))

    # If we have exactly 2 dominant peaks, likely digital
    if len(peaks) >= 2:
        # Sort by count
        peaks = sorted(peaks, key=lambda x: x[1], reverse=True)

        # Take top 2 peaks
        peak1, peak2 = peaks[0], peaks[1]

        # Check if these two peaks account for most samples
        total_in_peaks = peak1[1] + peak2[1]
        ratio = total_in_peaks / len(data)

        # Also check that peaks are well separated
        peak_separation = abs(peak1[2] - peak2[2])
        data_range = np.ptp(data)

        # Peaks should be separated by at least 30% of data range
        if ratio >= threshold_ratio and peak_separation > data_range * 0.3:
            low_level = min(peak1[2], peak2[2])
            high_level = max(peak1[2], peak2[2])

            confidence = min(0.95, ratio)

            return (
                True,
                {"low": float(low_level), "high": float(high_level)},
                confidence,
            )

    return False, None, 0.0


def _estimate_noise_level(data: NDArray[np.floating[Any]]) -> float:
    """Estimate noise level using median absolute deviation.

    Args:
        data: Signal data array.

    Returns:
        Estimated RMS noise level.
    """
    if len(data) < 10:
        return 0.0

    # Use differencing to remove slow variations
    diffs = np.diff(data)

    # MAD (Median Absolute Deviation) is robust to outliers
    median_diff = np.median(diffs)
    mad = np.median(np.abs(diffs - median_diff))

    # Convert MAD to RMS noise estimate
    # For Gaussian noise: sigma ~ 1.4826 * MAD
    # Divide by sqrt(2) because diff amplifies noise by sqrt(2)
    noise_estimate = (1.4826 * mad) / np.sqrt(2)

    return float(noise_estimate)


def _detect_periodicity(
    data: NDArray[np.floating[Any]],
    sample_rate: float,
    threshold: float,
) -> tuple[bool, float | None, float]:
    """Detect if signal is periodic using autocorrelation.

    Args:
        data: Signal data array.
        sample_rate: Sampling rate in Hz.
        threshold: Correlation threshold for periodic detection.

    Returns:
        Tuple of (is_periodic, period_seconds, confidence).
    """
    n = len(data)

    if n < 20:
        return False, None, 0.0

    # Remove DC for autocorrelation
    data_ac = data - np.mean(data)

    # Check if there's any variation
    if np.std(data_ac) < 1e-12:
        return False, None, 0.0

    # Compute autocorrelation for lags up to n-10 to detect signals with
    # ~1 period. Keep at least 10 samples of overlap for correlation
    max_lag = min(n - 10, 20000)  # Limit for performance

    autocorr = np.correlate(data_ac, data_ac, mode="full")
    autocorr = autocorr[n - 1 : n - 1 + max_lag]

    # Normalize
    if abs(autocorr[0]) > 1e-12:
        autocorr = autocorr / autocorr[0]
    else:
        return False, None, 0.0

    # Find peaks in autocorrelation (exclude lag=0 and very small lags)
    # Start searching from lag > n/100 to avoid noise
    min_lag = max(3, n // 100)
    peaks: list[tuple[int, float]] = []

    for i in range(min_lag, len(autocorr) - 2):
        # Use stronger peak detection
        if (
            autocorr[i] > autocorr[i - 1]
            and autocorr[i] > autocorr[i + 1]
            and autocorr[i] > autocorr[i - 2]
            and autocorr[i] > autocorr[i + 2]
        ):
            if autocorr[i] > threshold:
                peaks.append((i, autocorr[i]))

    if peaks:
        # Take first significant peak as period
        period_samples = peaks[0][0]
        confidence = float(peaks[0][1])

        period_seconds = period_samples / sample_rate

        return True, period_seconds, confidence

    return False, None, 0.0


def _count_edges(
    data: NDArray[np.floating[Any]],
    levels: dict[str, float] | None,
) -> int:
    """Count number of edges in signal.

    Args:
        data: Signal data array.
        levels: Optional digital levels dict with "low" and "high" keys.

    Returns:
        Number of edges detected.
    """
    if len(data) < 3:
        return 0

    if levels is not None:
        # Use provided levels
        threshold = (levels["low"] + levels["high"]) / 2
    else:
        # Use median as threshold
        threshold = float(np.median(data))

    # Find crossings
    above = data > threshold
    crossings = np.diff(above.astype(int))

    # Count non-zero crossings (both rising and falling)
    edge_count = np.sum(np.abs(crossings))

    return int(edge_count)


def _detect_periodicity_fft(
    data: NDArray[np.floating[Any]],
    sample_rate: float,
) -> tuple[bool, float | None, float]:
    """Detect periodicity using FFT (frequency domain analysis).

    This method works well for signals with few periods where autocorrelation
    may fail. It finds the dominant frequency component in the signal.

    Args:
        data: Signal data array.
        sample_rate: Sampling rate in Hz.

    Returns:
        Tuple of (is_periodic, period_seconds, confidence).
    """
    n = len(data)

    if n < 64:
        return False, None, 0.0

    # Remove DC component
    data_ac = data - np.mean(data)

    # Check if there's any variation
    if np.std(data_ac) < 1e-12:
        return False, None, 0.0

    # Compute FFT
    fft = np.fft.rfft(data_ac)
    freqs = np.fft.rfftfreq(n, 1.0 / sample_rate)

    # Compute power spectrum
    power = np.abs(fft) ** 2

    # Skip DC component (index 0)
    if len(power) < 3:
        return False, None, 0.0

    power = power[1:]
    freqs = freqs[1:]

    # Find peak in power spectrum
    peak_idx = np.argmax(power)
    peak_power = power[peak_idx]
    peak_freq = freqs[peak_idx]

    # Check if peak is significant compared to total power
    total_power = np.sum(power)
    if total_power < 1e-20:
        return False, None, 0.0

    power_ratio = peak_power / total_power

    # For periodic signals, the dominant frequency should have significant
    # power. Require at least 10% of total power in the peak.
    if power_ratio < 0.1:
        return False, None, 0.0

    # Check that frequency is reasonable (not too low or too high)
    nyquist = sample_rate / 2
    if peak_freq < sample_rate / n or peak_freq > nyquist * 0.9:
        return False, None, 0.0

    # Estimate period
    period_seconds = 1.0 / peak_freq

    # Confidence based on how dominant the peak is
    # High power ratio -> high confidence
    confidence = min(0.95, 0.5 + power_ratio)

    return True, period_seconds, float(confidence)


def _detect_edge_periodicity(
    data: NDArray[np.floating[Any]],
    sample_rate: float,
    levels: dict[str, float] | None,
) -> tuple[bool, float | None, float]:
    """Detect periodicity in digital signals by analyzing edge spacing.

    This method works well for signals with few periods where autocorrelation
    may fail. It detects regular patterns in edge timing.

    Requires at least 3 edges (2 intervals) to establish periodicity.
    A single pulse with only 2 edges is not sufficient evidence of periodic
    behavior.

    Args:
        data: Signal data array.
        sample_rate: Sampling rate in Hz.
        levels: Digital levels dict with "low" and "high" keys.

    Returns:
        Tuple of (is_periodic, period_seconds, confidence).
    """
    if len(data) < 10 or levels is None:
        return False, None, 0.0

    intervals = _extract_edge_intervals(data, levels)
    # Require at least 2 intervals (3 edges) for periodicity detection.
    # A single interval (2 edges) represents a single pulse, not periodicity.
    if intervals is None or len(intervals) < 2:
        return False, None, 0.0

    mean_interval_raw = np.mean(intervals)
    mean_interval: float = float(mean_interval_raw)
    if mean_interval < 1:
        return False, None, 0.0

    return _analyze_interval_pattern(intervals, mean_interval, sample_rate, len(data))


def _extract_edge_intervals(
    data: NDArray[np.floating[Any]], levels: dict[str, float]
) -> NDArray[np.intp] | None:
    """Extract intervals between edges.

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

    Returns:
        Array of edge intervals or None if insufficient edges.
    """
    threshold = (levels["low"] + levels["high"]) / 2
    above = data > threshold
    crossings = np.diff(above.astype(int))
    edge_positions = np.where(crossings != 0)[0]

    if len(edge_positions) < 2:
        return None

    return np.diff(edge_positions)


def _analyze_interval_pattern(
    intervals: NDArray[np.intp],
    mean_interval: float,
    sample_rate: float,
    n_samples: int,
) -> tuple[bool, float | None, float]:
    """Analyze interval pattern to detect periodicity.

    Args:
        intervals: Edge intervals array (must have length >= 2).
        mean_interval: Mean interval value.
        sample_rate: Sampling rate in Hz.
        n_samples: Total number of samples.

    Returns:
        Tuple of (is_periodic, period_seconds, confidence).
    """
    std_interval = np.std(intervals)
    cv = std_interval / mean_interval

    # High variation - check for alternating pattern
    if cv > 0.3:
        return _check_alternating_pattern(intervals, sample_rate)

    # Regular intervals - estimate period
    cv_float: float = float(cv)
    return _estimate_regular_period(mean_interval, cv_float, sample_rate, n_samples)


def _check_alternating_pattern(
    intervals: NDArray[np.intp], sample_rate: float
) -> tuple[bool, float | None, float]:
    """Check if intervals follow alternating pattern (square wave).

    Args:
        intervals: Edge intervals array.
        sample_rate: Sampling rate in Hz.

    Returns:
        Tuple of (is_periodic, period_seconds, confidence).
    """
    if len(intervals) >= 4:
        odd_intervals = intervals[::2]
        even_intervals = intervals[1::2]

        odd_cv = np.std(odd_intervals) / (np.mean(odd_intervals) + 1e-12)
        even_cv = np.std(even_intervals) / (np.mean(even_intervals) + 1e-12)

        if odd_cv < 0.2 and even_cv < 0.2:
            period_samples = np.mean(odd_intervals) + np.mean(even_intervals)
            period_seconds = period_samples / sample_rate
            confidence = 1.0 - max(odd_cv, even_cv)
            return True, period_seconds, float(confidence)

    elif len(intervals) == 2:
        period_samples = intervals[0] + intervals[1]
        period_seconds = period_samples / sample_rate
        return True, period_seconds, 0.75

    return False, None, 0.0


def _estimate_regular_period(
    mean_interval: float,
    cv: float,
    sample_rate: float,
    n_samples: int,
) -> tuple[bool, float | None, float]:
    """Estimate period from regular intervals.

    Args:
        mean_interval: Mean interval between edges.
        cv: Coefficient of variation.
        sample_rate: Sampling rate in Hz.
        n_samples: Total number of samples.

    Returns:
        Tuple of (is_periodic, period_seconds, confidence).
    """
    period_samples = 2 * mean_interval
    num_periods = n_samples / period_samples

    if num_periods >= 0.5:
        period_seconds = period_samples / sample_rate
        confidence = 1.0 - min(cv / 0.3, 0.5)
        return True, period_seconds, float(confidence)

    return False, None, 0.0
