"""Waveform analysis and signal processing for simulation data."""

import numpy as np


def compute_fft(time: np.ndarray, signal: np.ndarray, max_harmonics: int = 50) -> dict:
    """Compute FFT of a time-domain signal.

    Args:
        time: Time array in seconds (must be monotonically increasing)
        signal: Signal amplitude array (same length as time)
        max_harmonics: Maximum number of frequency bins to return

    Returns:
        Dict with frequencies, magnitudes, magnitudes_db,
        fundamental_freq, and dc_offset
    """
    if len(time) < 2 or len(signal) < 2:
        return {
            "frequencies": [],
            "magnitudes": [],
            "magnitudes_db": [],
            "fundamental_freq": 0.0,
            "dc_offset": 0.0,
        }

    n = len(signal)
    dt = (time[-1] - time[0]) / (n - 1)

    if dt <= 0:
        return {
            "frequencies": [],
            "magnitudes": [],
            "magnitudes_db": [],
            "fundamental_freq": 0.0,
            "dc_offset": float(np.mean(np.real(signal))),
        }

    # Use real FFT for real-valued signals
    spectrum = np.fft.rfft(np.real(signal))
    freqs = np.fft.rfftfreq(n, d=dt)
    magnitudes = np.abs(spectrum) * 2.0 / n

    # DC component doesn't get the 2x factor
    magnitudes[0] /= 2.0

    dc_offset = float(magnitudes[0])

    # Find fundamental: largest magnitude excluding DC
    if len(magnitudes) > 1:
        fund_idx = int(np.argmax(magnitudes[1:])) + 1
        fundamental_freq = float(freqs[fund_idx])
    else:
        fund_idx = 0
        fundamental_freq = 0.0

    # Trim to max_harmonics (plus DC bin)
    limit = min(max_harmonics + 1, len(freqs))
    freqs = freqs[:limit]
    magnitudes = magnitudes[:limit]

    # dB conversion with floor to avoid log(0)
    magnitudes_db = 20.0 * np.log10(np.maximum(magnitudes, 1e-15))

    return {
        "frequencies": freqs.tolist(),
        "magnitudes": magnitudes.tolist(),
        "magnitudes_db": magnitudes_db.tolist(),
        "fundamental_freq": fundamental_freq,
        "dc_offset": dc_offset,
    }


def compute_thd(time: np.ndarray, signal: np.ndarray, n_harmonics: int = 10) -> dict:
    """Compute Total Harmonic Distortion.

    THD is the ratio of harmonic content to the fundamental, expressed
    as a percentage: sqrt(sum(V_n^2 for n>=2)) / V_1 * 100

    Args:
        time: Time array in seconds
        signal: Signal amplitude array
        n_harmonics: Number of harmonics to include (2nd through nth)

    Returns:
        Dict with thd_percent, fundamental_freq, fundamental_magnitude,
        and harmonics list
    """
    if len(time) < 2 or len(signal) < 2:
        return {
            "thd_percent": 0.0,
            "fundamental_freq": 0.0,
            "fundamental_magnitude": 0.0,
            "harmonics": [],
        }

    n = len(signal)
    dt = (time[-1] - time[0]) / (n - 1)

    if dt <= 0:
        return {
            "thd_percent": 0.0,
            "fundamental_freq": 0.0,
            "fundamental_magnitude": 0.0,
            "harmonics": [],
        }

    spectrum = np.fft.rfft(np.real(signal))
    freqs = np.fft.rfftfreq(n, d=dt)
    magnitudes = np.abs(spectrum) * 2.0 / n
    magnitudes[0] /= 2.0  # DC correction

    # Fundamental = largest non-DC peak
    if len(magnitudes) <= 1:
        return {
            "thd_percent": 0.0,
            "fundamental_freq": 0.0,
            "fundamental_magnitude": 0.0,
            "harmonics": [],
        }

    fund_idx = int(np.argmax(magnitudes[1:])) + 1
    fundamental_freq = float(freqs[fund_idx])
    fundamental_mag = float(magnitudes[fund_idx])

    if fundamental_mag < 1e-15:
        return {
            "thd_percent": 0.0,
            "fundamental_freq": fundamental_freq,
            "fundamental_magnitude": fundamental_mag,
            "harmonics": [],
        }

    # Collect harmonics by finding the bin closest to each integer multiple
    harmonics = []
    harmonic_sum_sq = 0.0

    for h in range(2, n_harmonics + 2):
        target_freq = fundamental_freq * h
        if target_freq > freqs[-1]:
            break

        idx = int(np.argmin(np.abs(freqs - target_freq)))
        mag = float(magnitudes[idx])
        mag_db = 20.0 * np.log10(max(mag, 1e-15))
        harmonic_sum_sq += mag**2

        harmonics.append(
            {
                "harmonic": h,
                "frequency": float(freqs[idx]),
                "magnitude": mag,
                "magnitude_db": mag_db,
            }
        )

    thd_percent = (np.sqrt(harmonic_sum_sq) / fundamental_mag) * 100.0

    return {
        "thd_percent": float(thd_percent),
        "fundamental_freq": fundamental_freq,
        "fundamental_magnitude": fundamental_mag,
        "harmonics": harmonics,
    }


def compute_rms(signal: np.ndarray) -> float:
    """Compute RMS value of a signal.

    Args:
        signal: Signal amplitude array (real or complex)

    Returns:
        RMS value as a float
    """
    if len(signal) == 0:
        return 0.0

    real_signal = np.real(signal)
    return float(np.sqrt(np.mean(real_signal**2)))


def compute_peak_to_peak(signal: np.ndarray) -> dict:
    """Compute peak-to-peak metrics.

    Args:
        signal: Signal amplitude array

    Returns:
        Dict with peak_to_peak, max, min, and mean values
    """
    if len(signal) == 0:
        return {
            "peak_to_peak": 0.0,
            "max": 0.0,
            "min": 0.0,
            "mean": 0.0,
        }

    real_signal = np.real(signal)
    sig_max = float(np.max(real_signal))
    sig_min = float(np.min(real_signal))

    return {
        "peak_to_peak": sig_max - sig_min,
        "max": sig_max,
        "min": sig_min,
        "mean": float(np.mean(real_signal)),
    }


def compute_settling_time(
    time: np.ndarray,
    signal: np.ndarray,
    final_value: float | None = None,
    tolerance_percent: float = 2.0,
) -> dict:
    """Compute settling time.

    Searches backwards from the end of the signal to find the last
    point where the signal was outside the tolerance band around the
    final value. Settling time is measured from time[0] to that crossing.

    Args:
        time: Time array in seconds
        signal: Signal amplitude array
        final_value: Target value. If None, uses the last sample.
        tolerance_percent: Allowed deviation as a percentage of final_value
            (or absolute if final_value is near zero)

    Returns:
        Dict with settling_time, final_value, tolerance, and settled flag
    """
    if len(time) < 2 or len(signal) < 2:
        return {
            "settling_time": 0.0,
            "final_value": 0.0,
            "tolerance": 0.0,
            "settled": False,
        }

    real_signal = np.real(signal)

    if final_value is None:
        final_value = float(real_signal[-1])

    # Tolerance band: percentage of final_value, but use absolute
    # tolerance if final_value is near zero to avoid a degenerate band
    if abs(final_value) > 1e-12:
        tolerance = abs(final_value) * tolerance_percent / 100.0
    else:
        # Fall back to percentage of signal range
        sig_range = float(np.max(real_signal) - np.min(real_signal))
        tolerance = sig_range * tolerance_percent / 100.0 if sig_range > 0 else 1e-12

    # Walk backwards to find where signal last left the tolerance band
    outside = np.abs(real_signal - final_value) > tolerance

    if not np.any(outside):
        # Signal was always within tolerance
        return {
            "settling_time": 0.0,
            "final_value": final_value,
            "tolerance": tolerance,
            "settled": True,
        }

    # Find the last index that was outside the band
    last_outside = int(np.max(np.nonzero(outside)[0]))

    if last_outside >= len(time) - 1:
        # Never settled within the captured data
        return {
            "settling_time": float(time[-1] - time[0]),
            "final_value": final_value,
            "tolerance": tolerance,
            "settled": False,
        }

    # Settling time = time from start to the first sample inside the band
    # after the last excursion
    settling_time = float(time[last_outside + 1] - time[0])

    return {
        "settling_time": settling_time,
        "final_value": final_value,
        "tolerance": tolerance,
        "settled": True,
    }


def compute_rise_time(
    time: np.ndarray,
    signal: np.ndarray,
    low_pct: float = 10,
    high_pct: float = 90,
) -> dict:
    """Compute rise time between two percentage thresholds.

    Uses linear interpolation between samples for sub-sample accuracy.
    Thresholds are computed relative to the signal's min-to-max swing.

    Args:
        time: Time array in seconds
        signal: Signal amplitude array
        low_pct: Lower threshold as percentage of swing (default 10%)
        high_pct: Upper threshold as percentage of swing (default 90%)

    Returns:
        Dict with rise_time, low_threshold, high_threshold,
        low_time, and high_time
    """
    if len(time) < 2 or len(signal) < 2:
        return {
            "rise_time": 0.0,
            "low_threshold": 0.0,
            "high_threshold": 0.0,
            "low_time": 0.0,
            "high_time": 0.0,
        }

    real_signal = np.real(signal)
    sig_min = float(np.min(real_signal))
    sig_max = float(np.max(real_signal))
    swing = sig_max - sig_min

    if swing < 1e-15:
        return {
            "rise_time": 0.0,
            "low_threshold": sig_min,
            "high_threshold": sig_max,
            "low_time": float(time[0]),
            "high_time": float(time[0]),
        }

    low_thresh = sig_min + swing * (low_pct / 100.0)
    high_thresh = sig_min + swing * (high_pct / 100.0)

    low_time = _interpolate_crossing(time, real_signal, low_thresh, rising=True)
    high_time = _interpolate_crossing(time, real_signal, high_thresh, rising=True)

    if low_time is None or high_time is None:
        return {
            "rise_time": 0.0,
            "low_threshold": low_thresh,
            "high_threshold": high_thresh,
            "low_time": float(low_time) if low_time is not None else None,
            "high_time": float(high_time) if high_time is not None else None,
        }

    return {
        "rise_time": high_time - low_time,
        "low_threshold": low_thresh,
        "high_threshold": high_thresh,
        "low_time": low_time,
        "high_time": high_time,
    }


def _interpolate_crossing(
    time: np.ndarray,
    signal: np.ndarray,
    threshold: float,
    rising: bool = True,
) -> float | None:
    """Find the first time a signal crosses a threshold, with interpolation.

    Args:
        time: Time array
        signal: Signal array
        threshold: Value to cross
        rising: If True, look for low-to-high crossing

    Returns:
        Interpolated time of crossing, or None if no crossing found
    """
    for i in range(len(signal) - 1):
        if rising:
            crosses = signal[i] <= threshold < signal[i + 1]
        else:
            crosses = signal[i] >= threshold > signal[i + 1]

        if crosses:
            # Linear interpolation between samples
            dv = signal[i + 1] - signal[i]
            if abs(dv) < 1e-30:
                return float(time[i])
            frac = (threshold - signal[i]) / dv
            return float(time[i] + frac * (time[i + 1] - time[i]))

    return None


def compute_bandwidth(
    frequency: np.ndarray,
    magnitude_db: np.ndarray,
    ref_db: float | None = None,
) -> dict:
    """Compute -3dB bandwidth from frequency response data.

    Interpolates between data points to find the exact frequencies
    where the response crosses the -3dB level relative to the reference.

    Args:
        frequency: Frequency array in Hz (may be complex; .real is used)
        magnitude_db: Magnitude in dB
        ref_db: Reference level in dB. Defaults to the peak of magnitude_db.

    Returns:
        Dict with bandwidth_hz, f_low, f_high, ref_db, and type
    """
    if len(frequency) < 2 or len(magnitude_db) < 2:
        return {
            "bandwidth_hz": 0.0,
            "f_low": None,
            "f_high": None,
            "ref_db": 0.0,
            "type": "unknown",
        }

    freq = np.real(frequency).astype(np.float64)
    mag = np.real(magnitude_db).astype(np.float64)

    # Sort by frequency (in case data isn't ordered)
    sort_idx = np.argsort(freq)
    freq = freq[sort_idx]
    mag = mag[sort_idx]

    # Strip any negative frequencies
    positive_mask = freq >= 0
    freq = freq[positive_mask]
    mag = mag[positive_mask]

    if len(freq) < 2:
        return {
            "bandwidth_hz": 0.0,
            "f_low": None,
            "f_high": None,
            "ref_db": 0.0,
            "type": "unknown",
        }

    if ref_db is None:
        ref_db = float(np.max(mag))

    cutoff = ref_db - 3.0

    # Find all -3dB crossings by checking where magnitude crosses the cutoff
    above = mag >= cutoff
    crossings = []

    for i in range(len(mag) - 1):
        if above[i] != above[i + 1]:
            # Interpolate the exact crossing frequency
            dm = mag[i + 1] - mag[i]
            if abs(dm) < 1e-30:
                f_cross = float(freq[i])
            else:
                frac = (cutoff - mag[i]) / dm
                f_cross = float(freq[i] + frac * (freq[i + 1] - freq[i]))
            crossings.append(f_cross)

    if not crossings:
        # No crossing found - check if entirely above or below
        if np.all(above):
            return {
                "bandwidth_hz": float(freq[-1] - freq[0]),
                "f_low": None,
                "f_high": None,
                "ref_db": ref_db,
                "type": "unknown",
            }
        return {
            "bandwidth_hz": 0.0,
            "f_low": None,
            "f_high": None,
            "ref_db": ref_db,
            "type": "unknown",
        }

    # Classify response shape
    peak_idx = int(np.argmax(mag))

    if len(crossings) == 1:
        f_cross = crossings[0]
        if peak_idx < len(freq) // 2:
            # Peak is at low end => lowpass
            return {
                "bandwidth_hz": f_cross,
                "f_low": None,
                "f_high": f_cross,
                "ref_db": ref_db,
                "type": "lowpass",
            }
        else:
            # Peak is at high end => highpass
            return {
                "bandwidth_hz": float(freq[-1]) - f_cross,
                "f_low": f_cross,
                "f_high": None,
                "ref_db": ref_db,
                "type": "highpass",
            }

    # Two or more crossings => bandpass (use first and last)
    f_low = crossings[0]
    f_high = crossings[-1]

    return {
        "bandwidth_hz": f_high - f_low,
        "f_low": f_low,
        "f_high": f_high,
        "ref_db": ref_db,
        "type": "bandpass",
    }
