"""Protocol decoder handlers for pipeline system.

This module provides handlers for decoding various communication protocols.
All handlers follow the standard signature: (inputs, params, step_name) -> outputs.

Available Handlers:
    - decoder.uart: Decode UART/RS232 serial protocol
    - decoder.spi: Decode SPI (Serial Peripheral Interface)
    - decoder.i2c: Decode I2C (Inter-Integrated Circuit)
    - decoder.can: Decode CAN bus protocol
    - decoder.can_fd: Decode CAN-FD protocol
    - decoder.lin: Decode LIN bus protocol
    - decoder.flexray: Decode FlexRay protocol
    - decoder.onewire: Decode 1-Wire protocol
    - decoder.manchester: Decode Manchester encoded data
    - decoder.i2s: Decode I2S audio protocol
    - decoder.jtag: Decode JTAG debug interface
    - decoder.swd: Decode SWD (Serial Wire Debug)
    - decoder.usb: Decode USB protocol
    - decoder.hdlc: Decode HDLC frames
    - decoder.auto: Auto-detect and decode protocol
    - decoder.multi_protocol: Decode multiple protocols from multi-channel
"""

from __future__ import annotations

from typing import Any

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

# Lazy imports
_protocols = None
_convenience = None


def _get_protocols() -> Any:
    """Lazy import protocols module."""
    global _protocols
    if _protocols is None:
        from oscura.analyzers import protocols as _protocols_module

        _protocols = _protocols_module
    return _protocols


def _get_convenience() -> Any:
    """Lazy import convenience module."""
    global _convenience
    if _convenience is None:
        import oscura.convenience as _convenience_module

        _convenience = _convenience_module
    return _convenience


@register_handler("decoder.uart")
def handle_decoder_uart(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode UART/RS232 serial protocol.

    Inputs:
        trace: WaveformTrace or DigitalTrace to decode

    Parameters:
        baud_rate (int): Baud rate in bps (e.g., 9600, 115200)
        data_bits (int, optional): Number of data bits (default: 8)
        parity (str, optional): Parity type ('none', 'even', 'odd') (default: 'none')
        stop_bits (float, optional): Number of stop bits (default: 1.0)
        invert (bool, optional): Invert signal polarity (default: False)

    Outputs:
        frames: List of decoded UART frames
        num_frames: Number of frames decoded
        errors: List of errors encountered
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

    baud_rate = params.get("baud_rate")
    if baud_rate is None:
        raise PipelineExecutionError(
            "Missing required parameter 'baud_rate'. Suggestion: Add 'baud_rate: 115200' to params",
            step_name=step_name,
        )

    data_bits = params.get("data_bits", 8)
    parity = params.get("parity", "none")
    stop_bits = params.get("stop_bits", 1.0)
    invert = params.get("invert", False)

    try:
        decoder = protocols.UARTDecoder(
            baud_rate=baud_rate,
            data_bits=data_bits,
            parity=parity,
            stop_bits=stop_bits,
            invert=invert,
        )
        frames = decoder.decode(trace)

        # Compute statistics
        error_count = sum(1 for f in frames if getattr(f, "error", False))
        statistics = {
            "total_frames": len(frames),
            "errors": error_count,
            "success_rate": (len(frames) - error_count) / len(frames) if frames else 0,
        }
        errors = [f"Frame {i}: error" for i, f in enumerate(frames) if getattr(f, "error", False)]

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "errors": errors,
        "statistics": statistics,
    }


@register_handler("decoder.spi")
def handle_decoder_spi(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode SPI (Serial Peripheral Interface) protocol.

    Inputs:
        clk: Clock signal (DigitalTrace)
        mosi: MOSI (Master Out Slave In) signal (DigitalTrace, optional)
        miso: MISO (Master In Slave Out) signal (DigitalTrace, optional)
        cs: Chip Select signal (DigitalTrace, optional)

    Parameters:
        clock_edge (str, optional): Capture edge ('rising' or 'falling') (default: 'rising')
        bit_order (str, optional): Bit order ('msb' or 'lsb') (default: 'msb')
        word_size (int, optional): Word size in bits (default: 8)
        cs_active_low (bool, optional): CS is active low (default: True)

    Outputs:
        frames: List of decoded SPI frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

    clk = inputs.get("clk")
    if clk is None:
        raise PipelineExecutionError(
            "Missing required input 'clk'. Suggestion: Provide clock signal",
            step_name=step_name,
        )

    mosi = inputs.get("mosi")
    miso = inputs.get("miso")
    cs = inputs.get("cs")

    if mosi is None and miso is None:
        raise PipelineExecutionError(
            "At least one of 'mosi' or 'miso' required. Suggestion: Provide MOSI and/or MISO signals",
            step_name=step_name,
        )

    clock_edge = params.get("clock_edge", "rising")
    bit_order = params.get("bit_order", "msb")
    word_size = params.get("word_size", 8)
    cs_active_low = params.get("cs_active_low", True)

    try:
        decoder = protocols.SPIDecoder(
            clock_edge=clock_edge,
            bit_order=bit_order,
            word_size=word_size,
            cs_active_low=cs_active_low,
        )
        frames = decoder.decode(clk, mosi=mosi, miso=miso, cs=cs)

        statistics = {
            "total_frames": len(frames),
            "total_bytes": sum(len(f.data) if hasattr(f, "data") else 0 for f in frames),
        }

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.i2c")
def handle_decoder_i2c(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode I2C (Inter-Integrated Circuit) protocol.

    Inputs:
        scl: I2C clock signal (DigitalTrace)
        sda: I2C data signal (DigitalTrace)

    Parameters:
        address_bits (int, optional): Address width (7 or 10) (default: 7)

    Outputs:
        frames: List of decoded I2C frames
        num_frames: Number of frames decoded
        addresses: Set of I2C addresses seen
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

    scl = inputs.get("scl")
    sda = inputs.get("sda")

    if scl is None or sda is None:
        raise PipelineExecutionError(
            "Missing required inputs 'scl' and 'sda'. Suggestion: Provide both SCL and SDA signals",
            step_name=step_name,
        )

    address_bits = params.get("address_bits", 7)

    try:
        decoder = protocols.I2CDecoder(address_bits=address_bits)
        frames = decoder.decode(scl, sda)

        # Extract addresses
        addresses = set()
        for f in frames:
            if hasattr(f, "address"):
                addresses.add(f.address)

        statistics = {
            "total_frames": len(frames),
            "unique_addresses": len(addresses),
        }

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "addresses": sorted(addresses),
        "statistics": statistics,
    }


@register_handler("decoder.can")
def handle_decoder_can(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode CAN bus protocol.

    Inputs:
        trace: CAN bus signal (WaveformTrace or DigitalTrace)

    Parameters:
        bit_rate (int): CAN bit rate in bps (e.g., 125000, 250000, 500000, 1000000)
        sample_point (float, optional): Sample point (0-1) (default: 0.75)

    Outputs:
        frames: List of decoded CAN frames
        num_frames: Number of frames decoded
        can_ids: Set of CAN IDs seen
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

    bit_rate = params.get("bit_rate")
    if bit_rate is None:
        raise PipelineExecutionError(
            "Missing required parameter 'bit_rate'. Suggestion: Add 'bit_rate: 500000' to params",
            step_name=step_name,
        )

    sample_point = params.get("sample_point", 0.75)

    try:
        decoder = protocols.CANDecoder(bit_rate=bit_rate, sample_point=sample_point)
        frames = decoder.decode(trace)

        # Extract CAN IDs
        can_ids = set()
        for f in frames:
            if hasattr(f, "can_id"):
                can_ids.add(f.can_id)

        statistics = {
            "total_frames": len(frames),
            "unique_ids": len(can_ids),
        }

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "can_ids": sorted(can_ids),
        "statistics": statistics,
    }


@register_handler("decoder.can_fd")
def handle_decoder_can_fd(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode CAN-FD protocol.

    Inputs:
        trace: CAN-FD bus signal

    Parameters:
        nominal_bit_rate (int): Nominal bit rate in bps
        data_bit_rate (int): Data phase bit rate in bps
        sample_point (float, optional): Sample point (0-1) (default: 0.75)

    Outputs:
        frames: List of decoded CAN-FD frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

    nominal_bit_rate = params.get("nominal_bit_rate")
    data_bit_rate = params.get("data_bit_rate")

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

    sample_point = params.get("sample_point", 0.75)

    try:
        decoder = protocols.CANFDDecoder(
            nominal_bit_rate=nominal_bit_rate,
            data_bit_rate=data_bit_rate,
            sample_point=sample_point,
        )
        frames = decoder.decode(trace)

        statistics = {"total_frames": len(frames)}

    except Exception as e:
        raise PipelineExecutionError(f"CAN-FD decoding failed: {e}", step_name=step_name) from e

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.lin")
def handle_decoder_lin(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode LIN bus protocol.

    Inputs:
        trace: LIN bus signal

    Parameters:
        baud_rate (int): LIN baud rate in bps (typically 9600 or 19200)

    Outputs:
        frames: List of decoded LIN frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

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

    try:
        decoder = protocols.LINDecoder(baud_rate=baud_rate)
        frames = decoder.decode(trace)

        statistics = {"total_frames": len(frames)}

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.flexray")
def handle_decoder_flexray(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode FlexRay protocol.

    Inputs:
        trace: FlexRay bus signal

    Parameters:
        channel (str): FlexRay channel ('A' or 'B')
        bit_rate (int): Bit rate in bps (typically 10000000)

    Outputs:
        frames: List of decoded FlexRay frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

    channel = params.get("channel", "A")
    bit_rate = params.get("bit_rate", 10000000)

    try:
        decoder = protocols.FlexRayDecoder(channel=channel, bit_rate=bit_rate)
        frames = decoder.decode(trace)

        statistics = {"total_frames": len(frames)}

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.onewire")
def handle_decoder_onewire(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode 1-Wire protocol.

    Inputs:
        trace: 1-Wire bus signal

    Parameters:
        None

    Outputs:
        frames: List of decoded 1-Wire frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

    try:
        decoder = protocols.OneWireDecoder()
        frames = decoder.decode(trace)

        statistics = {"total_frames": len(frames)}

    except Exception as e:
        raise PipelineExecutionError(f"1-Wire decoding failed: {e}", step_name=step_name) from e

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.manchester")
def handle_decoder_manchester(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode Manchester encoded data.

    Inputs:
        trace: Manchester encoded signal

    Parameters:
        bit_rate (int): Bit rate in bps
        ieee_convention (bool, optional): Use IEEE 802.3 convention (default: True)

    Outputs:
        frames: List of decoded frames
        num_frames: Number of frames decoded
        data: Decoded bit stream
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

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

    ieee_convention = params.get("ieee_convention", True)

    try:
        decoder = protocols.ManchesterDecoder(bit_rate=bit_rate, ieee_convention=ieee_convention)
        frames = decoder.decode(trace)
        data = b"".join(f.data if hasattr(f, "data") else b"" for f in frames)

        statistics = {
            "total_frames": len(frames),
            "total_bits": len(data) * 8,
        }

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "data": data,
        "statistics": statistics,
    }


@register_handler("decoder.i2s")
def handle_decoder_i2s(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode I2S (Inter-IC Sound) audio protocol.

    Inputs:
        sck: Serial clock (bit clock)
        ws: Word select (frame clock)
        sd: Serial data

    Parameters:
        bits_per_sample (int, optional): Bits per sample (default: 16)

    Outputs:
        frames: List of decoded audio frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

    sck = inputs.get("sck")
    ws = inputs.get("ws")
    sd = inputs.get("sd")

    if sck is None or ws is None or sd is None:
        raise PipelineExecutionError(
            "Missing required inputs 'sck', 'ws', 'sd'", step_name=step_name
        )

    bits_per_sample = params.get("bits_per_sample", 16)

    try:
        decoder = protocols.I2SDecoder(bits_per_sample=bits_per_sample)
        frames = decoder.decode(sck, ws, sd)

        statistics = {"total_frames": len(frames)}

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.jtag")
def handle_decoder_jtag(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode JTAG debug interface.

    Inputs:
        tck: Test clock
        tms: Test mode select
        tdi: Test data in
        tdo: Test data out (optional)

    Parameters:
        None

    Outputs:
        frames: List of decoded JTAG frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

    tck = inputs.get("tck")
    tms = inputs.get("tms")
    tdi = inputs.get("tdi")
    tdo = inputs.get("tdo")

    if tck is None or tms is None or tdi is None:
        raise PipelineExecutionError(
            "Missing required inputs 'tck', 'tms', 'tdi'", step_name=step_name
        )

    try:
        decoder = protocols.JTAGDecoder()
        frames = decoder.decode(tck, tms, tdi, tdo)

        statistics = {"total_frames": len(frames)}

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.swd")
def handle_decoder_swd(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode SWD (Serial Wire Debug) protocol.

    Inputs:
        swclk: Serial wire clock
        swdio: Serial wire data I/O

    Parameters:
        None

    Outputs:
        frames: List of decoded SWD frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

    swclk = inputs.get("swclk")
    swdio = inputs.get("swdio")

    if swclk is None or swdio is None:
        raise PipelineExecutionError(
            "Missing required inputs 'swclk' and 'swdio'", step_name=step_name
        )

    try:
        decoder = protocols.SWDDecoder()
        frames = decoder.decode(swclk, swdio)

        statistics = {"total_frames": len(frames)}

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.usb")
def handle_decoder_usb(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode USB protocol.

    Inputs:
        dp: USB D+ signal
        dm: USB D- signal

    Parameters:
        speed (str, optional): USB speed ('low', 'full', 'high') (default: 'full')

    Outputs:
        frames: List of decoded USB packets
        num_frames: Number of packets decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

    dp = inputs.get("dp")
    dm = inputs.get("dm")

    if dp is None or dm is None:
        raise PipelineExecutionError("Missing required inputs 'dp' and 'dm'", step_name=step_name)

    speed = params.get("speed", "full")

    try:
        decoder = protocols.USBDecoder(speed=speed)
        frames = decoder.decode(dp, dm)

        statistics = {"total_frames": len(frames)}

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.hdlc")
def handle_decoder_hdlc(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode HDLC frames.

    Inputs:
        trace: HDLC signal

    Parameters:
        None

    Outputs:
        frames: List of decoded HDLC frames
        num_frames: Number of frames decoded
        statistics: Decoding statistics dict
    """
    protocols = _get_protocols()

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

    try:
        decoder = protocols.HDLCDecoder()
        frames = decoder.decode(trace)

        statistics = {"total_frames": len(frames)}

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

    return {
        "frames": frames,
        "num_frames": len(frames),
        "statistics": statistics,
    }


@register_handler("decoder.auto")
def handle_decoder_auto(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Auto-detect and decode protocol.

    Inputs:
        trace: WaveformTrace or DigitalTrace

    Parameters:
        protocol (str, optional): Force specific protocol (None for auto-detect)
        min_confidence (float, optional): Minimum detection confidence (0-1) (default: 0.5)

    Outputs:
        protocol: Detected protocol name
        frames: List of decoded frames
        confidence: Detection confidence (0-1)
        baud_rate: Detected baud/bit rate (if applicable)
        config: Protocol configuration parameters
        errors: List of decoding errors
        statistics: Decoding statistics
    """
    convenience = _get_convenience()

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

    protocol = params.get("protocol")
    min_confidence = params.get("min_confidence", 0.5)

    try:
        result = convenience.auto_decode(trace, protocol=protocol, min_confidence=min_confidence)

        # Extract all fields from DecodeResult
        return {
            "protocol": result.protocol,
            "frames": result.frames,
            "confidence": result.confidence,
            "baud_rate": result.baud_rate,
            "config": result.config,
            "errors": result.errors,
            "statistics": result.statistics,
        }

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


@register_handler("decoder.multi_protocol")
def handle_decoder_multi_protocol(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Decode multiple protocols from multi-channel trace.

    Inputs:
        channels: Dict mapping channel names to traces

    Parameters:
        protocols (dict): Dict mapping protocol names to decoder configs
            Example: {"uart": {"channel": "CH1", "baud_rate": 115200},
                      "spi": {"clk": "CH2", "mosi": "CH3"}}

    Outputs:
        results: Dict mapping protocol names to decode results
        summary: Summary statistics across all protocols
    """
    # This is a meta-handler that dispatches to other handlers
    channels = inputs.get("channels")
    if channels is None:
        raise PipelineExecutionError("Missing required input 'channels'", step_name=step_name)

    protocol_configs = params.get("protocols")
    if not protocol_configs:
        raise PipelineExecutionError(
            "Missing required parameter 'protocols'. Suggestion: Specify protocols dict with decoder configs",
            step_name=step_name,
        )

    results = {}
    total_frames = 0

    for proto_name, proto_config in protocol_configs.items():
        try:
            # Get handler for this protocol
            from oscura.pipeline.handlers import get_handler

            handler_type = f"decoder.{proto_name}"
            handler = get_handler(handler_type)

            if handler is None:
                raise PipelineExecutionError(
                    f"Unknown protocol: {proto_name}. Suggestion: Supported protocols: uart, spi, i2c, can, etc.",
                    step_name=step_name,
                )

            # Map channel names to actual traces
            channel_name = proto_config.get("channel")
            if channel_name:
                proto_inputs = {"trace": channels.get(channel_name)}
            else:
                # Multi-signal protocols (SPI, I2C, etc.)
                proto_inputs = {}
                for key in ["clk", "mosi", "miso", "cs", "scl", "sda", "dp", "dm"]:
                    if key in proto_config:
                        ch_name = proto_config[key]
                        proto_inputs[key] = channels.get(ch_name)

            # Decode this protocol
            result = handler(proto_inputs, proto_config, step_name)
            results[proto_name] = result
            total_frames += result.get("num_frames", 0)

        except Exception as e:
            results[proto_name] = {"error": str(e)}

    summary = {
        "protocols_decoded": len([r for r in results.values() if "error" not in r]),
        "total_frames": total_frames,
    }

    return {
        "results": results,
        "summary": summary,
    }
