"""Filter handlers for pipeline system.

This module provides handlers for applying various digital filters to signals.
All handlers follow the standard signature: (inputs, params, step_name) -> outputs.

Available Handlers:
    - filter.low_pass: Apply low-pass filter
    - filter.high_pass: Apply high-pass filter
    - filter.band_pass: Apply band-pass filter
    - filter.band_stop: Apply band-stop/notch filter
    - filter.butterworth: Apply Butterworth filter (generic)
    - filter.chebyshev: Apply Chebyshev Type 1 filter
    - filter.bessel: Apply Bessel filter
    - filter.moving_average: Apply moving average filter
    - filter.savitzky_golay: Apply Savitzky-Golay smoothing
    - filter.median: Apply median filter
    - filter.gaussian: Apply Gaussian smoothing
    - filter.notch: Apply narrow notch filter at specific frequency
"""

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
_filtering = None


def _get_filtering() -> Any:
    """Lazy import filtering module."""
    global _filtering
    if _filtering is None:
        import oscura.utils.filtering as _filtering_module

        _filtering = _filtering_module
    return _filtering


@register_handler("filter.low_pass")
def handle_filter_low_pass(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply low-pass filter to remove high-frequency components.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        cutoff (float): Cutoff frequency in Hz
        order (int, optional): Filter order (default: 4)
        filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
            'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')

    Outputs:
        trace: Filtered WaveformTrace
        cutoff: Applied cutoff frequency
        order: Filter order used
        filter_type: Filter type used

    Example:
        >>> # Remove noise above 1 MHz
        >>> result = handle_filter_low_pass(
        ...     {"trace": my_trace},
        ...     {"cutoff": 1e6, "order": 6},
        ...     "lpf"
        ... )
    """
    filtering = _get_filtering()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace' - provide WaveformTrace",
            step_name=step_name,
        )

    cutoff = params.get("cutoff")
    if cutoff is None:
        raise PipelineExecutionError(
            "Missing required parameter 'cutoff' - add 'cutoff: 1e6' (frequency in Hz)",
            step_name=step_name,
        )

    order = params.get("order", 4)
    filter_type = params.get("filter_type", "butterworth")

    try:
        filtered_trace = filtering.low_pass(
            trace,
            cutoff=cutoff,
            order=order,
            filter_type=filter_type,
        )
    except Exception as e:
        raise PipelineExecutionError(f"Low-pass filtering failed: {e}", step_name=step_name) from e

    return {
        "trace": filtered_trace,
        "cutoff": cutoff,
        "order": order,
        "filter_type": filter_type,
    }


@register_handler("filter.high_pass")
def handle_filter_high_pass(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply high-pass filter to remove low-frequency components and DC offset.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        cutoff (float): Cutoff frequency in Hz
        order (int, optional): Filter order (default: 4)
        filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
            'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')

    Outputs:
        trace: Filtered WaveformTrace
        cutoff: Applied cutoff frequency
        order: Filter order used
        filter_type: Filter type used

    Example:
        >>> # Remove DC and frequencies below 100 Hz
        >>> result = handle_filter_high_pass(
        ...     {"trace": my_trace},
        ...     {"cutoff": 100, "order": 4},
        ...     "hpf"
        ... )
    """
    filtering = _get_filtering()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace' - provide WaveformTrace",
            step_name=step_name,
        )

    cutoff = params.get("cutoff")
    if cutoff is None:
        raise PipelineExecutionError(
            "Missing required parameter 'cutoff' - add 'cutoff: 100' (frequency in Hz)",
            step_name=step_name,
        )

    order = params.get("order", 4)
    filter_type = params.get("filter_type", "butterworth")

    try:
        filtered_trace = filtering.high_pass(
            trace,
            cutoff=cutoff,
            order=order,
            filter_type=filter_type,
        )
    except Exception as e:
        raise PipelineExecutionError(f"High-pass filtering failed: {e}", step_name=step_name) from e

    return {
        "trace": filtered_trace,
        "cutoff": cutoff,
        "order": order,
        "filter_type": filter_type,
    }


@register_handler("filter.band_pass")
def handle_filter_band_pass(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply band-pass filter to pass frequencies in a specified band.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        low (float): Lower cutoff frequency in Hz
        high (float): Upper cutoff frequency in Hz
        order (int, optional): Filter order (default: 4)
        filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
            'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')

    Outputs:
        trace: Filtered WaveformTrace
        low: Lower cutoff frequency
        high: Upper cutoff frequency
        order: Filter order used
        filter_type: Filter type used

    Example:
        >>> # Pass only 1 kHz to 10 kHz
        >>> result = handle_filter_band_pass(
        ...     {"trace": my_trace},
        ...     {"low": 1e3, "high": 10e3, "order": 6},
        ...     "bpf"
        ... )
    """
    filtering = _get_filtering()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace' - provide WaveformTrace",
            step_name=step_name,
        )

    low = params.get("low")
    high = params.get("high")

    if low is None or high is None:
        raise PipelineExecutionError(
            "Missing required parameters 'low' and 'high' - add frequencies in Hz",
            step_name=step_name,
        )

    if low >= high:
        raise PipelineExecutionError(
            f"Low cutoff ({low}) must be less than high cutoff ({high})",
            step_name=step_name,
        )

    order = params.get("order", 4)
    filter_type = params.get("filter_type", "butterworth")

    try:
        filtered_trace = filtering.band_pass(
            trace,
            low=low,
            high=high,
            order=order,
            filter_type=filter_type,
        )
    except Exception as e:
        raise PipelineExecutionError(f"Band-pass filtering failed: {e}", step_name=step_name) from e

    return {
        "trace": filtered_trace,
        "low": low,
        "high": high,
        "order": order,
        "filter_type": filter_type,
    }


@register_handler("filter.band_stop")
def handle_filter_band_stop(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply band-stop (notch) filter to reject frequencies in a specified band.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        low (float): Lower cutoff frequency in Hz
        high (float): Upper cutoff frequency in Hz
        order (int, optional): Filter order (default: 4)
        filter_type (str, optional): Filter type - 'butterworth', 'chebyshev1',
            'chebyshev2', 'bessel', or 'elliptic' (default: 'butterworth')

    Outputs:
        trace: Filtered WaveformTrace
        low: Lower cutoff frequency
        high: Upper cutoff frequency
        order: Filter order used
        filter_type: Filter type used

    Example:
        >>> # Remove 50 Hz power line interference
        >>> result = handle_filter_band_stop(
        ...     {"trace": my_trace},
        ...     {"low": 49, "high": 51, "order": 4},
        ...     "bsf"
        ... )
    """
    filtering = _get_filtering()

    trace = inputs.get("trace")
    if trace is None:
        raise PipelineExecutionError(
            "Missing required input 'trace' - provide WaveformTrace",
            step_name=step_name,
        )

    low = params.get("low")
    high = params.get("high")

    if low is None or high is None:
        raise PipelineExecutionError(
            "Missing required parameters 'low' and 'high' - add frequencies in Hz",
            step_name=step_name,
        )

    if low >= high:
        raise PipelineExecutionError(
            f"Low cutoff ({low}) must be less than high cutoff ({high})",
            step_name=step_name,
        )

    order = params.get("order", 4)
    filter_type = params.get("filter_type", "butterworth")

    try:
        filtered_trace = filtering.band_stop(
            trace,
            low=low,
            high=high,
            order=order,
            filter_type=filter_type,
        )
    except Exception as e:
        raise PipelineExecutionError(f"Band-stop filtering failed: {e}", step_name=step_name) from e

    return {
        "trace": filtered_trace,
        "low": low,
        "high": high,
        "order": order,
        "filter_type": filter_type,
    }


@register_handler("filter.butterworth")
def handle_filter_butterworth(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply generic Butterworth filter with specified band type.

    Butterworth filters have maximally flat passband response.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        cutoff (float|tuple): Cutoff frequency in Hz (scalar for low/high-pass,
            tuple (low, high) for band-pass/band-stop)
        band_type (str): Filter band type - 'lowpass', 'highpass', 'bandpass', or 'bandstop'
        order (int, optional): Filter order (default: 4)

    Outputs:
        trace: Filtered WaveformTrace
        cutoff: Applied cutoff frequency
        band_type: Filter band type used
        order: Filter order used

    Example:
        >>> # Generic low-pass Butterworth
        >>> result = handle_filter_butterworth(
        ...     {"trace": my_trace},
        ...     {"cutoff": 1e6, "band_type": "lowpass", "order": 6},
        ...     "butter"
        ... )
    """
    filtering = _get_filtering()

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

    cutoff = params.get("cutoff")
    if cutoff is None:
        raise PipelineExecutionError(
            "Missing required parameter 'cutoff'",
            step_name=step_name,
        )

    band_type = params.get("band_type", "lowpass")
    if band_type not in ["lowpass", "highpass", "bandpass", "bandstop"]:
        raise PipelineExecutionError(
            f"Invalid band_type '{band_type}' - use lowpass/highpass/bandpass/bandstop",
            step_name=step_name,
        )

    order = params.get("order", 4)

    try:
        # Create Butterworth filter
        sample_rate = trace.metadata.sample_rate
        filt = filtering.ButterworthFilter(
            cutoff=cutoff,
            sample_rate=sample_rate,
            order=order,
            band_type=band_type,
        )
        filtered_trace = filt.apply(trace)

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

    return {
        "trace": filtered_trace,
        "cutoff": cutoff,
        "band_type": band_type,
        "order": order,
    }


@register_handler("filter.chebyshev")
def handle_filter_chebyshev(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply Chebyshev Type 1 filter with specified band type.

    Chebyshev Type 1 filters have ripple in the passband but steeper rolloff
    than Butterworth filters.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        cutoff (float|tuple): Cutoff frequency in Hz (scalar for low/high-pass,
            tuple (low, high) for band-pass/band-stop)
        band_type (str): Filter band type - 'lowpass', 'highpass', 'bandpass', or 'bandstop'
        order (int, optional): Filter order (default: 4)
        ripple (float, optional): Passband ripple in dB (default: 1.0)

    Outputs:
        trace: Filtered WaveformTrace
        cutoff: Applied cutoff frequency
        band_type: Filter band type used
        order: Filter order used
        ripple: Passband ripple in dB

    Example:
        >>> # Chebyshev low-pass with 0.5 dB ripple
        >>> result = handle_filter_chebyshev(
        ...     {"trace": my_trace},
        ...     {"cutoff": 1e6, "band_type": "lowpass", "ripple": 0.5},
        ...     "cheby"
        ... )
    """
    filtering = _get_filtering()

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

    cutoff = params.get("cutoff")
    if cutoff is None:
        raise PipelineExecutionError("Missing required parameter 'cutoff'", step_name=step_name)

    band_type = params.get("band_type", "lowpass")
    if band_type not in ["lowpass", "highpass", "bandpass", "bandstop"]:
        raise PipelineExecutionError(
            f"Invalid band_type '{band_type}' - use lowpass/highpass/bandpass/bandstop",
            step_name=step_name,
        )

    order = params.get("order", 4)
    ripple = params.get("ripple", 1.0)

    try:
        sample_rate = trace.metadata.sample_rate
        filt = filtering.ChebyshevType1Filter(
            cutoff=cutoff,
            sample_rate=sample_rate,
            order=order,
            band_type=band_type,
            ripple=ripple,
        )
        filtered_trace = filt.apply(trace)

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

    return {
        "trace": filtered_trace,
        "cutoff": cutoff,
        "band_type": band_type,
        "order": order,
        "ripple": ripple,
    }


@register_handler("filter.bessel")
def handle_filter_bessel(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply Bessel filter with specified band type.

    Bessel filters have maximally flat group delay (linear phase response),
    preserving pulse shapes better than other filter types.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        cutoff (float|tuple): Cutoff frequency in Hz (scalar for low/high-pass,
            tuple (low, high) for band-pass/band-stop)
        band_type (str): Filter band type - 'lowpass', 'highpass', 'bandpass', or 'bandstop'
        order (int, optional): Filter order (default: 4)

    Outputs:
        trace: Filtered WaveformTrace
        cutoff: Applied cutoff frequency
        band_type: Filter band type used
        order: Filter order used

    Example:
        >>> # Bessel low-pass for minimal pulse distortion
        >>> result = handle_filter_bessel(
        ...     {"trace": my_trace},
        ...     {"cutoff": 1e6, "band_type": "lowpass", "order": 5},
        ...     "bessel"
        ... )
    """
    filtering = _get_filtering()

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

    cutoff = params.get("cutoff")
    if cutoff is None:
        raise PipelineExecutionError("Missing required parameter 'cutoff'", step_name=step_name)

    band_type = params.get("band_type", "lowpass")
    if band_type not in ["lowpass", "highpass", "bandpass", "bandstop"]:
        raise PipelineExecutionError(
            f"Invalid band_type '{band_type}' - use lowpass/highpass/bandpass/bandstop",
            step_name=step_name,
        )

    order = params.get("order", 4)

    try:
        sample_rate = trace.metadata.sample_rate
        filt = filtering.BesselFilter(
            cutoff=cutoff,
            sample_rate=sample_rate,
            order=order,
            band_type=band_type,
        )
        filtered_trace = filt.apply(trace)

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

    return {
        "trace": filtered_trace,
        "cutoff": cutoff,
        "band_type": band_type,
        "order": order,
    }


@register_handler("filter.moving_average")
def handle_filter_moving_average(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply moving average (FIR) filter for simple smoothing.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        window_size (int): Number of samples in averaging window (must be odd for 'same' mode)
        mode (str, optional): Convolution mode - 'same' (default), 'valid', or 'full'

    Outputs:
        trace: Filtered WaveformTrace
        window_size: Window size used
        mode: Convolution mode used

    Example:
        >>> # Simple 11-sample moving average
        >>> result = handle_filter_moving_average(
        ...     {"trace": my_trace},
        ...     {"window_size": 11},
        ...     "mavg"
        ... )
    """
    filtering = _get_filtering()

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

    window_size = params.get("window_size")
    if window_size is None:
        raise PipelineExecutionError(
            "Missing required parameter 'window_size'",
            step_name=step_name,
        )

    if not isinstance(window_size, int) or window_size < 1:
        raise PipelineExecutionError(
            f"Window size must be a positive integer, got {window_size}",
            step_name=step_name,
        )

    mode = params.get("mode", "same")

    try:
        filtered_trace = filtering.moving_average(
            trace,
            window_size=window_size,
            mode=mode,
        )
    except Exception as e:
        raise PipelineExecutionError(
            f"Moving average filtering failed: {e}", step_name=step_name
        ) from e

    return {
        "trace": filtered_trace,
        "window_size": window_size,
        "mode": mode,
    }


@register_handler("filter.savitzky_golay")
def handle_filter_savitzky_golay(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply Savitzky-Golay smoothing filter.

    Smooths data while preserving higher moments (peaks, etc.) better than
    simple moving average. Uses polynomial fitting within a moving window.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        window_length (int): Length of filter window (must be odd and > polyorder)
        polyorder (int): Order of polynomial used in fitting
        deriv (int, optional): Derivative order (0 = smoothing, 1 = first derivative, etc.)
            (default: 0)

    Outputs:
        trace: Filtered WaveformTrace
        window_length: Window length used
        polyorder: Polynomial order used
        deriv: Derivative order

    Example:
        >>> # Savitzky-Golay smoothing with cubic polynomial
        >>> result = handle_filter_savitzky_golay(
        ...     {"trace": my_trace},
        ...     {"window_length": 11, "polyorder": 3},
        ...     "savgol"
        ... )
    """
    filtering = _get_filtering()

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

    window_length = params.get("window_length")
    polyorder = params.get("polyorder")

    if window_length is None or polyorder is None:
        raise PipelineExecutionError(
            "Missing required parameters 'window_length' and 'polyorder'",
            step_name=step_name,
        )

    if not isinstance(window_length, int) or window_length < 1:
        raise PipelineExecutionError(
            f"Window length must be a positive integer, got {window_length}",
            step_name=step_name,
        )

    if not isinstance(polyorder, int) or polyorder < 0:
        raise PipelineExecutionError(
            f"Polynomial order must be a non-negative integer, got {polyorder}",
            step_name=step_name,
        )

    deriv = params.get("deriv", 0)

    try:
        filtered_trace = filtering.savgol_filter(
            trace,
            window_length=window_length,
            polyorder=polyorder,
            deriv=deriv,
        )
    except Exception as e:
        raise PipelineExecutionError(
            f"Savitzky-Golay filtering failed: {e}", step_name=step_name
        ) from e

    return {
        "trace": filtered_trace,
        "window_length": window_length,
        "polyorder": polyorder,
        "deriv": deriv,
    }


@register_handler("filter.median")
def handle_filter_median(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply median filter for spike/impulse noise removal.

    Non-linear filter that preserves edges while removing outliers.
    Excellent for removing salt-and-pepper noise or voltage spikes.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        kernel_size (int): Size of the median filter kernel (must be odd)

    Outputs:
        trace: Filtered WaveformTrace
        kernel_size: Kernel size used

    Example:
        >>> # Remove impulse noise with 5-sample median filter
        >>> result = handle_filter_median(
        ...     {"trace": my_trace},
        ...     {"kernel_size": 5},
        ...     "median"
        ... )
    """
    filtering = _get_filtering()

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

    kernel_size = params.get("kernel_size")
    if kernel_size is None:
        raise PipelineExecutionError(
            "Missing required parameter 'kernel_size'",
            step_name=step_name,
        )

    if not isinstance(kernel_size, int) or kernel_size < 1:
        raise PipelineExecutionError(
            f"Kernel size must be a positive integer, got {kernel_size}",
            step_name=step_name,
        )

    try:
        filtered_trace = filtering.median_filter(
            trace,
            kernel_size=kernel_size,
        )
    except Exception as e:
        raise PipelineExecutionError(f"Median filtering failed: {e}", step_name=step_name) from e

    return {
        "trace": filtered_trace,
        "kernel_size": kernel_size,
    }


@register_handler("filter.gaussian")
def handle_filter_gaussian(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply Gaussian smoothing filter.

    Smooths with Gaussian kernel of specified standard deviation.
    Provides optimal time-frequency localization.

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        sigma (float): Standard deviation of Gaussian kernel in samples

    Outputs:
        trace: Filtered WaveformTrace
        sigma: Standard deviation used

    Example:
        >>> # Gaussian smoothing with sigma=3
        >>> result = handle_filter_gaussian(
        ...     {"trace": my_trace},
        ...     {"sigma": 3.0},
        ...     "gaussian"
        ... )
    """
    filtering = _get_filtering()

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

    sigma = params.get("sigma")
    if sigma is None:
        raise PipelineExecutionError(
            "Missing required parameter 'sigma'",
            step_name=step_name,
        )

    if not isinstance(sigma, (int, float)) or sigma <= 0:
        raise PipelineExecutionError(
            f"Sigma must be a positive number, got {sigma}",
            step_name=step_name,
        )

    try:
        filtered_trace = filtering.gaussian_filter(
            trace,
            sigma=sigma,
        )
    except Exception as e:
        raise PipelineExecutionError(f"Gaussian filtering failed: {e}", step_name=step_name) from e

    return {
        "trace": filtered_trace,
        "sigma": sigma,
    }


@register_handler("filter.notch")
def handle_filter_notch(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Apply narrow notch filter at specific frequency.

    Uses band-stop Butterworth filter with bandwidth determined by Q factor.
    Ideal for removing specific interference (e.g., 50/60 Hz power line noise).

    Inputs:
        trace: WaveformTrace to filter

    Parameters:
        freq (float): Center frequency to notch out in Hz
        q_factor (float, optional): Quality factor (higher = narrower notch) (default: 30.0)

    Outputs:
        trace: Filtered WaveformTrace
        freq: Notch frequency
        q_factor: Quality factor used
        bandwidth: Actual bandwidth in Hz (freq / q_factor)

    Example:
        >>> # Remove 60 Hz power line noise
        >>> result = handle_filter_notch(
        ...     {"trace": my_trace},
        ...     {"freq": 60, "q_factor": 30},
        ...     "notch_60hz"
        ... )
    """
    filtering = _get_filtering()

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

    freq = params.get("freq")
    if freq is None:
        raise PipelineExecutionError(
            "Missing required parameter 'freq'",
            step_name=step_name,
        )

    if not isinstance(freq, (int, float)) or freq <= 0:
        raise PipelineExecutionError(
            f"Frequency must be a positive number, got {freq}",
            step_name=step_name,
        )

    q_factor = params.get("q_factor", 30.0)

    try:
        filtered_trace = filtering.notch_filter(
            trace,
            freq=freq,
            q_factor=q_factor,
        )

        # Calculate bandwidth for output
        bandwidth = freq / q_factor

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

    return {
        "trace": filtered_trace,
        "freq": freq,
        "q_factor": q_factor,
        "bandwidth": bandwidth,
    }
