"""Analysis handlers for pipeline system.

This module provides handlers for signal analysis including waveform measurements,
spectral analysis, power analysis, timing measurements, jitter analysis, and statistics.
All handlers follow the standard signature: (inputs, params, step_name) -> outputs.

Available Handlers:
    - analysis.waveform: Basic waveform measurements (rise/fall time, frequency, amplitude)
    - analysis.spectral: Spectral analysis (FFT, PSD, THD, SNR, SINAD, ENOB, SFDR)
    - analysis.power: Power analysis (voltage, current, active/reactive/apparent power)
    - analysis.timing: IEEE 181 pulse timing measurements and clock recovery
    - analysis.jitter: Jitter analysis (TIE, period jitter, cycle-to-cycle)
    - analysis.statistics: Statistical measurements (mean, std, histogram, outliers)
    - analysis.fft: FFT analysis returning frequency and magnitude arrays
    - analysis.psd: Power spectral density calculation
    - analysis.thd: Total harmonic distortion measurement
    - analysis.eye_diagram: Eye diagram analysis for digital signals
    - analysis.auto: Auto-detect signal type and perform appropriate analysis
"""

from __future__ import annotations

from typing import Any

from oscura.core.config.pipeline import PipelineExecutionError
from oscura.pipeline.handlers import register_handler

# Lazy imports to avoid circular dependencies
_waveform = None
_spectral = None
_power = None
_jitter = None
_statistics = None
_eye = None
_signal = None


def _get_waveform() -> Any:
    """Lazy import waveform analyzer module."""
    global _waveform
    if _waveform is None:
        from oscura.analyzers import waveform as _waveform_module

        _waveform = _waveform_module
    return _waveform


def _get_spectral() -> Any:
    """Lazy import spectral analyzer module."""
    global _spectral
    if _spectral is None:
        from oscura.analyzers import spectral as _spectral_module

        _spectral = _spectral_module
    return _spectral


def _get_power() -> Any:
    """Lazy import power analyzer module."""
    global _power
    if _power is None:
        from oscura.analyzers import power as _power_module

        _power = _power_module
    return _power


def _get_jitter() -> Any:
    """Lazy import jitter analyzer module."""
    global _jitter
    if _jitter is None:
        from oscura.analyzers import jitter as _jitter_module

        _jitter = _jitter_module
    return _jitter


def _get_statistics() -> Any:
    """Lazy import statistics analyzer module."""
    global _statistics
    if _statistics is None:
        from oscura.analyzers import statistics as _statistics_module

        _statistics = _statistics_module
    return _statistics


def _get_eye() -> Any:
    """Lazy import eye diagram analyzer module."""
    global _eye
    if _eye is None:
        from oscura.analyzers import eye as _eye_module

        _eye = _eye_module
    return _eye


def _get_signal() -> Any:
    """Lazy import signal analyzer module."""
    global _signal
    if _signal is None:
        from oscura.analyzers import signal as _signal_module

        _signal = _signal_module
    return _signal


@register_handler("analysis.waveform")
def handle_analysis_waveform(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Perform basic waveform measurements.

    Inputs:
        trace: WaveformTrace to analyze

    Parameters:
        measurements (list[str], optional): Specific measurements to perform
            Available: rise_time, fall_time, frequency, amplitude, rms, duty_cycle,
                      period, pulse_width, overshoot, undershoot, preshoot, mean
            Default: all measurements
        threshold_low (float, optional): Low threshold percentage (default: 10%)
        threshold_mid (float, optional): Mid threshold percentage (default: 50%)
        threshold_high (float, optional): High threshold percentage (default: 90%)

    Outputs:
        measurements: Dict of measurement name to value
        rise_time: Rise time in seconds (if measured)
        fall_time: Fall time in seconds (if measured)
        frequency: Frequency in Hz (if measured)
        amplitude: Peak-to-peak amplitude (if measured)
        rms: RMS value (if measured)
        duty_cycle: Duty cycle percentage (if measured)
        period: Period in seconds (if measured)
        pulse_width: Pulse width in seconds (if measured)
        mean: Mean value (if measured)
        overshoot: Overshoot percentage (if measured)
        undershoot: Undershoot percentage (if measured)
    """
    waveform = _get_waveform()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
            step_name=step_name,
        )

    requested_measurements = params.get("measurements")
    threshold_low = params.get("threshold_low", 0.1)
    threshold_mid = params.get("threshold_mid", 0.5)
    threshold_high = params.get("threshold_high", 0.9)

    # Define all available measurements
    all_measurements = [
        "rise_time",
        "fall_time",
        "frequency",
        "amplitude",
        "rms",
        "duty_cycle",
        "period",
        "pulse_width",
        "overshoot",
        "undershoot",
        "preshoot",
        "mean",
    ]

    # Use requested or all measurements
    measurements_to_run = requested_measurements or all_measurements

    try:
        results = {}
        measurements_dict = {}

        for measurement in measurements_to_run:
            try:
                if measurement == "rise_time":
                    value = waveform.rise_time(trace, low=threshold_low, high=threshold_high)
                    results["rise_time"] = value
                    measurements_dict["rise_time"] = value
                elif measurement == "fall_time":
                    value = waveform.fall_time(trace, low=threshold_low, high=threshold_high)
                    results["fall_time"] = value
                    measurements_dict["fall_time"] = value
                elif measurement == "frequency":
                    value = waveform.frequency(trace)
                    results["frequency"] = value
                    measurements_dict["frequency"] = value
                elif measurement == "amplitude":
                    value = waveform.amplitude(trace)
                    results["amplitude"] = value
                    measurements_dict["amplitude"] = value
                elif measurement == "rms":
                    value = waveform.rms(trace)
                    results["rms"] = value
                    measurements_dict["rms"] = value
                elif measurement == "duty_cycle":
                    value = waveform.duty_cycle(trace, threshold=threshold_mid)
                    results["duty_cycle"] = value
                    measurements_dict["duty_cycle"] = value
                elif measurement == "period":
                    value = waveform.period(trace)
                    results["period"] = value
                    measurements_dict["period"] = value
                elif measurement == "pulse_width":
                    value = waveform.pulse_width(trace, threshold=threshold_mid)
                    results["pulse_width"] = value
                    measurements_dict["pulse_width"] = value
                elif measurement == "overshoot":
                    value = waveform.overshoot(trace)
                    results["overshoot"] = value
                    measurements_dict["overshoot"] = value
                elif measurement == "undershoot":
                    value = waveform.undershoot(trace)
                    results["undershoot"] = value
                    measurements_dict["undershoot"] = value
                elif measurement == "preshoot":
                    value = waveform.preshoot(trace)
                    results["preshoot"] = value
                    measurements_dict["preshoot"] = value
                elif measurement == "mean":
                    value = waveform.mean(trace)
                    results["mean"] = value
                    measurements_dict["mean"] = value
            except Exception as e:
                # Log measurement failure but continue with others
                measurements_dict[f"{measurement}_error"] = str(e)

        results["measurements"] = measurements_dict

    except Exception as e:
        raise PipelineExecutionError(f"Waveform analysis failed: {e}", step_name=step_name) from e

    return results


@register_handler("analysis.spectral")
def handle_analysis_spectral(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Perform spectral analysis with quality metrics.

    Inputs:
        trace: WaveformTrace to analyze

    Parameters:
        window (str, optional): FFT window function (default: 'hann')
        nfft (int, optional): FFT length (default: trace length)
        fundamental_freq (float, optional): Fundamental frequency for THD/SNR
        num_harmonics (int, optional): Number of harmonics for THD (default: 5)

    Outputs:
        frequencies: FFT frequency array
        magnitudes: FFT magnitude array (linear)
        thd_db: Total harmonic distortion in dB
        snr_db: Signal-to-noise ratio in dB
        sinad_db: Signal-to-noise and distortion ratio in dB
        enob: Effective number of bits
        sfdr_db: Spurious-free dynamic range in dB
        fundamental_power: Power at fundamental frequency
        fundamental_idx: Index of fundamental frequency peak
    """
    spectral = _get_spectral()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
            step_name=step_name,
        )

    window = params.get("window", "hann")
    nfft = params.get("nfft")
    fundamental_freq = params.get("fundamental_freq")
    num_harmonics = params.get("num_harmonics", 5)

    try:
        # Perform FFT
        frequencies, magnitudes = spectral.fft(trace, window=window, nfft=nfft)

        results = {
            "frequencies": frequencies,
            "magnitudes": magnitudes,
        }

        # Calculate quality metrics if fundamental frequency provided
        if fundamental_freq:
            thd_value = spectral.thd(
                trace, fundamental_freq=fundamental_freq, harmonics=num_harmonics
            )
            results["thd_db"] = thd_value

            snr_value = spectral.snr(trace, fundamental_freq=fundamental_freq)
            results["snr_db"] = snr_value

            sinad_value = spectral.sinad(trace, fundamental_freq=fundamental_freq)
            results["sinad_db"] = sinad_value

            enob_value = spectral.enob(trace, fundamental_freq=fundamental_freq)
            results["enob"] = enob_value

            sfdr_value = spectral.sfdr(trace, fundamental_freq=fundamental_freq)
            results["sfdr_db"] = sfdr_value

            # Find fundamental frequency peak
            freq_resolution = frequencies[1] - frequencies[0]
            fund_idx = int(fundamental_freq / freq_resolution)
            results["fundamental_idx"] = fund_idx
            results["fundamental_power"] = float(magnitudes[fund_idx] ** 2)

    except Exception as e:
        raise PipelineExecutionError(f"Spectral analysis failed: {e}", step_name=step_name) from e

    return results


@register_handler("analysis.power")
def handle_analysis_power(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Perform power analysis on voltage and current traces.

    Inputs:
        voltage: Voltage WaveformTrace
        current: Current WaveformTrace

    Parameters:
        fundamental_freq (float, optional): Fundamental frequency for AC analysis

    Outputs:
        instantaneous_power: Power trace (voltage * current)
        average_power: Average power in watts
        peak_power: Peak instantaneous power
        rms_power: RMS power
        energy: Total energy in joules
        active_power: Active (real) power in watts (if fundamental_freq provided)
        reactive_power: Reactive power in VAR (if fundamental_freq provided)
        apparent_power: Apparent power in VA (if fundamental_freq provided)
        power_factor: Power factor (if fundamental_freq provided)
        displacement_power_factor: Displacement power factor (if fundamental_freq provided)
        distortion_power_factor: Distortion power factor (if fundamental_freq provided)
    """
    power = _get_power()

    voltage = inputs.get("voltage")
    current = inputs.get("current")

    if voltage is None or current is None:
        raise PipelineExecutionError(
            "Missing required inputs 'voltage' and 'current'. Suggestion: Provide both voltage and current WaveformTraces",
            step_name=step_name,
        )

    fundamental_freq = params.get("fundamental_freq")

    try:
        # Basic power measurements
        power_trace = power.instantaneous_power(voltage, current)
        avg_power = power.average_power(voltage, current)
        peak = power.peak_power(voltage, current)
        rms_pwr = power.rms_power(voltage, current)
        energy_val = power.energy(voltage, current)

        results = {
            "instantaneous_power": power_trace,
            "average_power": avg_power,
            "peak_power": peak,
            "rms_power": rms_pwr,
            "energy": energy_val,
        }

        # AC power analysis if fundamental frequency provided
        if fundamental_freq:
            active_pwr = power.average_power(voltage, current)  # Same as average
            reactive_pwr = power.reactive_power(voltage, current, fundamental_freq)
            apparent_pwr = power.apparent_power(voltage, current)
            pf = power.power_factor(voltage, current, fundamental_freq)
            dpf = power.displacement_power_factor(voltage, current, fundamental_freq)
            distortion_pf = power.distortion_power_factor(voltage, current, fundamental_freq)

            results.update(
                {
                    "active_power": active_pwr,
                    "reactive_power": reactive_pwr,
                    "apparent_power": apparent_pwr,
                    "power_factor": pf,
                    "displacement_power_factor": dpf,
                    "distortion_power_factor": distortion_pf,
                }
            )

    except Exception as e:
        raise PipelineExecutionError(f"Power analysis failed: {e}", step_name=step_name) from e

    return results


@register_handler("analysis.timing")
def handle_analysis_timing(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Perform IEEE 181 pulse timing measurements and clock recovery.

    Inputs:
        trace: WaveformTrace or DigitalTrace to analyze

    Parameters:
        method (str, optional): Clock recovery method
            ('zcd', 'histogram', 'autocorrelation', 'pll', 'fft')
            Default: 'autocorrelation'
        expected_freq (float, optional): Expected clock frequency for validation

    Outputs:
        detected_clock_rate: Recovered clock frequency in Hz
        confidence: Clock recovery confidence (0-1)
        jitter_rms: RMS jitter in seconds
        drift_rate: Clock drift in ppm
        snr_db: Signal-to-noise ratio in dB
        method: Method used for clock recovery
        statistics: Additional timing statistics
    """
    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace or DigitalTrace to analyze",
            step_name=step_name,
        )

    method = params.get("method", "autocorrelation")
    expected_freq = params.get("expected_freq")

    try:
        # Import TimingAnalyzer
        from oscura.analyzers.signal.timing_analysis import TimingAnalyzer

        analyzer = TimingAnalyzer(method=method)
        result = analyzer.recover_clock(trace.data, trace.metadata.sample_rate)

        outputs = {
            "detected_clock_rate": result.detected_clock_rate,
            "confidence": result.confidence,
            "jitter_rms": result.jitter_rms,
            "drift_rate": result.drift_rate,
            "snr_db": result.snr_db,
            "method": result.method,
            "statistics": result.statistics,
        }

        # Validate against expected frequency if provided
        if expected_freq:
            freq_error_pct = abs(result.detected_clock_rate - expected_freq) / expected_freq * 100
            outputs["frequency_error_percent"] = freq_error_pct
            outputs["frequency_match"] = freq_error_pct < 1.0  # Within 1%

    except Exception as e:
        raise PipelineExecutionError(f"Timing analysis failed: {e}", step_name=step_name) from e

    return outputs


@register_handler("analysis.jitter")
def handle_analysis_jitter(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Perform jitter analysis on digital signals.

    Inputs:
        trace: DigitalTrace or WaveformTrace to analyze
        edges (array, optional): Pre-computed edge times

    Parameters:
        unit_interval (float, optional): Unit interval in seconds (required for TIE)
        decompose (bool, optional): Perform jitter decomposition into RJ/DJ (default: False)
        bathtub_ber (float, optional): Target BER for bathtub curve (e.g., 1e-12)

    Outputs:
        tie: Time interval error array (if unit_interval provided)
        period_jitter: Period jitter in seconds
        cycle_jitter: Cycle-to-cycle jitter in seconds
        dcd: Duty cycle distortion in seconds
        rj_rms: Random jitter RMS (if decompose=True)
        dj_pp: Deterministic jitter peak-to-peak (if decompose=True)
        tj_at_ber: Total jitter at target BER (if bathtub_ber provided)
        bathtub_positions: Bathtub curve positions (if bathtub_ber provided)
        bathtub_ber_values: Bathtub curve BER values (if bathtub_ber provided)
    """
    jitter = _get_jitter()

    trace = inputs.get("trace")
    edges = inputs.get("edges")

    if trace is None and edges is None:
        raise PipelineExecutionError(
            "Missing required input 'trace' or 'edges'. Suggestion: Provide either a trace or pre-computed edge times",
            step_name=step_name,
        )

    unit_interval = params.get("unit_interval")
    decompose = params.get("decompose", False)
    bathtub_ber = params.get("bathtub_ber")

    try:
        results = {}

        # Extract edges if not provided
        if edges is None:
            import numpy as np

            from oscura.analyzers.digital.edges import detect_edges

            # trace is guaranteed non-None here due to validation at line 494
            assert trace is not None  # Help mypy understand the guarantee
            edge_list = detect_edges(trace.data, sample_rate=trace.metadata.sample_rate)
            edges = np.array([edge.time for edge in edge_list])

        # Period jitter
        period_jitter_result = jitter.period_jitter(edges)
        results["period_jitter"] = period_jitter_result.rms_jitter

        # Cycle-to-cycle jitter
        cycle_jitter_result = jitter.cycle_to_cycle_jitter(edges)
        results["cycle_jitter"] = cycle_jitter_result.rms_jitter

        # Duty cycle distortion
        dcd_result = jitter.measure_dcd(edges)
        results["dcd"] = dcd_result.dcd_mean

        # TIE calculation if unit interval provided
        if unit_interval:
            tie = jitter.tie_from_edges(edges, unit_interval)
            results["tie"] = tie

            # Jitter decomposition
            if decompose:
                decomposition = jitter.decompose_jitter(tie)
                results["rj_rms"] = decomposition.rj.rj_rms
                results["dj_pp"] = decomposition.dj.dj_pp

                # Total jitter at BER if requested
                if bathtub_ber:
                    tj = jitter.tj_at_ber(
                        rj_rms=decomposition.rj.rj_rms,
                        dj_pp=decomposition.dj.dj_pp,
                        ber=bathtub_ber,
                    )
                    results["tj_at_ber"] = tj

            # Bathtub curve
            if bathtub_ber:
                bathtub_result = jitter.bathtub_curve(tie, unit_interval)
                results["bathtub_positions"] = bathtub_result.sample_positions
                results["bathtub_ber_values"] = bathtub_result.ber_values

    except Exception as e:
        raise PipelineExecutionError(f"Jitter analysis failed: {e}", step_name=step_name) from e

    return results


@register_handler("analysis.statistics")
def handle_analysis_statistics(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Perform statistical measurements on signal data.

    Inputs:
        trace: WaveformTrace to analyze

    Parameters:
        outliers (bool, optional): Detect outliers (default: False)
        outlier_method (str, optional): Outlier detection method
            ('zscore', 'modified_zscore', 'iqr') (default: 'modified_zscore')
        histogram_bins (int, optional): Number of histogram bins (default: 50)
        percentiles (list[float], optional): Percentiles to calculate (default: [25, 50, 75])

    Outputs:
        mean: Mean value
        std: Standard deviation
        min: Minimum value
        max: Maximum value
        median: Median value
        variance: Variance
        skewness: Skewness
        kurtosis: Kurtosis
        percentiles: Dict of percentile values
        histogram_counts: Histogram bin counts
        histogram_edges: Histogram bin edges
        outlier_indices: Indices of outliers (if outliers=True)
        outlier_count: Number of outliers detected (if outliers=True)
    """
    statistics = _get_statistics()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
            step_name=step_name,
        )

    detect_outliers_flag = params.get("outliers", False)
    outlier_method = params.get("outlier_method", "modified_zscore")
    histogram_bins = params.get("histogram_bins", 50)
    percentiles_list = params.get("percentiles", [25, 50, 75])

    try:
        # Basic statistics
        stats = statistics.summary_stats(trace.data)

        results = {
            "mean": stats.mean,
            "std": stats.std,
            "min": stats.min,
            "max": stats.max,
            "median": stats.median,
            "variance": stats.variance,
            "skewness": stats.skewness,
            "kurtosis": stats.kurtosis,
        }

        # Percentiles
        percentile_values = statistics.percentiles(trace.data, percentiles_list)
        results["percentiles"] = {
            f"p{int(p)}": v for p, v in zip(percentiles_list, percentile_values, strict=True)
        }

        # Histogram
        hist_result = statistics.histogram(trace.data, bins=histogram_bins)
        results["histogram_counts"] = hist_result.counts
        results["histogram_edges"] = hist_result.edges

        # Outlier detection
        if detect_outliers_flag:
            outlier_result = statistics.detect_outliers(trace.data, method=outlier_method)
            results["outlier_indices"] = outlier_result.indices
            results["outlier_count"] = len(outlier_result.indices)

    except Exception as e:
        raise PipelineExecutionError(
            f"Statistical analysis failed: {e}", step_name=step_name
        ) from e

    return results


@register_handler("analysis.fft")
def handle_analysis_fft(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Perform FFT analysis returning frequency and magnitude arrays.

    Inputs:
        trace: WaveformTrace to analyze

    Parameters:
        window (str, optional): FFT window function (default: 'hann')
            Options: 'hann', 'hamming', 'blackman', 'bartlett', 'kaiser', 'rectangular'
        nfft (int, optional): FFT length (default: trace length)
        return_phase (bool, optional): Also return phase information (default: False)

    Outputs:
        frequencies: Frequency array in Hz
        magnitudes: Magnitude array (linear scale)
        magnitudes_db: Magnitude array in dB (20*log10)
        phases: Phase array in radians (if return_phase=True)
        peak_freq: Frequency of peak magnitude
        peak_magnitude: Peak magnitude value
    """
    spectral = _get_spectral()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
            step_name=step_name,
        )

    window = params.get("window", "hann")
    nfft = params.get("nfft")
    return_phase = params.get("return_phase", False)

    try:
        # Perform FFT
        frequencies, magnitudes = spectral.fft(trace, window=window, nfft=nfft)

        import numpy as np

        # Convert to dB
        magnitudes_db = 20 * np.log10(magnitudes + 1e-12)  # Avoid log(0)

        # Find peak
        peak_idx = np.argmax(magnitudes)
        peak_freq = float(frequencies[peak_idx])
        peak_mag = float(magnitudes[peak_idx])

        results = {
            "frequencies": frequencies,
            "magnitudes": magnitudes,
            "magnitudes_db": magnitudes_db,
            "peak_freq": peak_freq,
            "peak_magnitude": peak_mag,
        }

        # Phase information if requested
        if return_phase:
            fft_result = np.fft.rfft(trace.data, n=nfft)
            phases = np.angle(fft_result)
            results["phases"] = phases

    except Exception as e:
        raise PipelineExecutionError(f"FFT analysis failed: {e}", step_name=step_name) from e

    return results


@register_handler("analysis.psd")
def handle_analysis_psd(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Calculate power spectral density.

    Inputs:
        trace: WaveformTrace to analyze

    Parameters:
        method (str, optional): PSD estimation method ('periodogram', 'welch', 'bartlett')
            Default: 'periodogram'
        nfft (int, optional): FFT length for PSD calculation
        window (str, optional): Window function (default: 'hann')

    Outputs:
        frequencies: Frequency array in Hz
        psd: Power spectral density array (V^2/Hz or A^2/Hz)
        psd_db: PSD in dB scale (10*log10)
        total_power: Total power integrated across frequency
    """
    spectral = _get_spectral()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
            step_name=step_name,
        )

    method = params.get("method", "periodogram")
    nfft = params.get("nfft")
    window = params.get("window", "hann")

    try:
        # Calculate PSD based on method
        if method == "periodogram":
            frequencies, psd_values = spectral.periodogram(trace, nfft=nfft, window=window)
        elif method == "welch":
            frequencies, psd_values = spectral.psd(trace, nfft=nfft, window=window)
        elif method == "bartlett":
            frequencies, psd_values = spectral.bartlett_psd(trace, nfft=nfft)
        else:
            raise ValueError(f"Unknown PSD method: {method}")

        import numpy as np

        # Convert to dB
        psd_db = 10 * np.log10(psd_values + 1e-20)  # Avoid log(0)

        # Total power (integrate PSD)
        freq_spacing = frequencies[1] - frequencies[0]
        total_power = float(np.sum(psd_values) * freq_spacing)

        results = {
            "frequencies": frequencies,
            "psd": psd_values,
            "psd_db": psd_db,
            "total_power": total_power,
        }

    except Exception as e:
        raise PipelineExecutionError(f"PSD calculation failed: {e}", step_name=step_name) from e

    return results


@register_handler("analysis.thd")
def handle_analysis_thd(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Calculate total harmonic distortion.

    Inputs:
        trace: WaveformTrace to analyze

    Parameters:
        fundamental_freq (float): Fundamental frequency in Hz
        num_harmonics (int, optional): Number of harmonics to analyze (default: 5)

    Outputs:
        thd_db: Total harmonic distortion in dB
        thd_percent: THD as percentage
        harmonic_powers: Array of harmonic power values
        harmonic_frequencies: Array of harmonic frequencies
        fundamental_power: Power at fundamental frequency
    """
    spectral = _get_spectral()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace to analyze",
            step_name=step_name,
        )

    fundamental_freq = params.get("fundamental_freq")
    if fundamental_freq is None:
        raise PipelineExecutionError(
            "Missing required parameter 'fundamental_freq'. Suggestion: Specify the fundamental frequency for THD calculation",
            step_name=step_name,
        )

    num_harmonics = params.get("num_harmonics", 5)

    try:
        # Calculate THD
        thd_db = spectral.thd(trace, fundamental_freq=fundamental_freq, harmonics=num_harmonics)

        # Convert to percentage
        thd_percent = 100 * (10 ** (thd_db / 20))

        # Get FFT to extract harmonic powers
        frequencies, magnitudes = spectral.fft(trace)

        freq_resolution = frequencies[1] - frequencies[0]
        harmonic_powers = []
        harmonic_frequencies = []

        for h in range(1, num_harmonics + 2):  # Include fundamental + harmonics
            harmonic_freq = fundamental_freq * h
            harmonic_idx = int(harmonic_freq / freq_resolution)
            if harmonic_idx < len(magnitudes):
                harmonic_powers.append(float(magnitudes[harmonic_idx] ** 2))
                harmonic_frequencies.append(harmonic_freq)

        fundamental_power = harmonic_powers[0] if harmonic_powers else 0.0

        results = {
            "thd_db": thd_db,
            "thd_percent": thd_percent,
            "harmonic_powers": harmonic_powers[1:],  # Exclude fundamental
            "harmonic_frequencies": harmonic_frequencies[1:],
            "fundamental_power": fundamental_power,
        }

    except Exception as e:
        raise PipelineExecutionError(f"THD calculation failed: {e}", step_name=step_name) from e

    return results


@register_handler("analysis.eye_diagram")
def handle_analysis_eye_diagram(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Generate and analyze eye diagram for digital signals.

    Inputs:
        trace: WaveformTrace or DigitalTrace to analyze

    Parameters:
        unit_interval (float): Unit interval (bit period) in seconds
        samples_per_ui (int, optional): Samples per unit interval (default: 100)
        threshold (float, optional): Decision threshold (default: auto)

    Outputs:
        eye_diagram: 2D array of eye diagram samples
        eye_height: Eye height (voltage units)
        eye_width: Eye width (time units)
        eye_opening: Eye opening at center (voltage units)
        q_factor: Q-factor (signal quality metric)
        crossing_percentage: Percentage of ideal crossing point
        jitter_rms: RMS jitter at threshold crossings
    """
    eye = _get_eye()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a WaveformTrace or DigitalTrace to analyze",
            step_name=step_name,
        )

    unit_interval = params.get("unit_interval")
    if unit_interval is None:
        raise PipelineExecutionError(
            "Missing required parameter 'unit_interval'. Suggestion: Specify the unit interval (bit period) in seconds",
            step_name=step_name,
        )

    samples_per_ui = params.get("samples_per_ui", 100)
    threshold = params.get("threshold")

    try:
        # Generate eye diagram
        eye_result = eye.generate_eye(trace, unit_interval, samples_per_ui=samples_per_ui)

        # Calculate eye metrics
        eye_metrics = eye.measure_eye(eye_result, threshold=threshold)

        results = {
            "eye_diagram": eye_result.data,
            "eye_height": eye_metrics.height,
            "eye_width": eye_metrics.width,
            "eye_opening": eye_metrics.opening,
            "q_factor": eye_metrics.q_factor,
            "crossing_percentage": eye_metrics.crossing_percentage,
            "jitter_rms": eye_metrics.jitter_rms,
        }

    except Exception as e:
        raise PipelineExecutionError(
            f"Eye diagram analysis failed: {e}", step_name=step_name
        ) from e

    return results


@register_handler("analysis.auto")
def handle_analysis_auto(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Auto-detect signal type and perform appropriate analysis.

    Automatically determines signal characteristics and performs relevant measurements.

    Inputs:
        trace: WaveformTrace or DigitalTrace to analyze

    Parameters:
        detailed (bool, optional): Include all available measurements (default: False)

    Outputs:
        signal_type: Detected signal type ('analog', 'digital', 'mixed')
        frequency: Dominant frequency if detected
        measurements: Dict of relevant measurements
        recommendations: List of suggested additional analyses
    """
    waveform = _get_waveform()
    statistics = _get_statistics()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace'. Suggestion: Provide a trace to analyze",
            step_name=step_name,
        )

    detailed = params.get("detailed", False)

    try:
        import numpy as np

        # Detect signal type
        data = trace.data
        unique_values = len(np.unique(data))
        data_range = np.ptp(data)

        # Simple heuristic: digital if few unique values
        if unique_values <= 10 and data_range > 0:
            signal_type = "digital"
        elif unique_values > 10:
            signal_type = "analog"
        else:
            signal_type = "unknown"

        measurements = {}
        recommendations = []

        # Frequency detection
        try:
            freq = waveform.frequency(trace)
            measurements["frequency"] = freq
        except Exception:
            # Best-effort: frequency detection may fail on noisy/aperiodic signals
            freq = None

        # Basic statistics
        stats = statistics.summary_stats(data)
        measurements.update(
            {
                "mean": stats.mean,
                "std": stats.std,
                "min": stats.min,
                "max": stats.max,
            }
        )

        # Additional measurements based on signal type
        if signal_type == "analog":
            measurements["amplitude"] = waveform.amplitude(trace)
            measurements["rms"] = waveform.rms(trace)

            if detailed:
                try:
                    measurements["rise_time"] = waveform.rise_time(trace)
                    measurements["fall_time"] = waveform.fall_time(trace)
                except Exception:
                    # Best-effort: rise/fall time may fail on slow/noisy edges
                    pass

            recommendations.extend(
                [
                    "analysis.spectral - for frequency domain analysis",
                    "analysis.waveform - for detailed timing measurements",
                ]
            )

        elif signal_type == "digital":
            try:
                measurements["duty_cycle"] = waveform.duty_cycle(trace)
            except Exception:
                # Best-effort: duty cycle may fail on aperiodic signals
                pass

            recommendations.extend(
                [
                    "analysis.jitter - for jitter analysis",
                    "analysis.eye_diagram - if data rate is known",
                    "decoder.auto - to identify protocol",
                ]
            )

        # Suggest power analysis if signal looks like voltage or current
        if data_range > 0.1:  # Arbitrary threshold
            recommendations.append("analysis.power - if you have both voltage and current traces")

        results = {
            "signal_type": signal_type,
            "frequency": freq,
            "measurements": measurements,
            "recommendations": recommendations,
        }

    except Exception as e:
        raise PipelineExecutionError(f"Auto-analysis failed: {e}", step_name=step_name) from e

    return results
