"""Output exporter handlers for pipeline system.

This module provides handlers for exporting data to various file formats.
All handlers follow the standard signature: (inputs, params, step_name) -> outputs.

Available Handlers:
    - output.json: Export to JSON file
    - output.csv: Export to CSV file
    - output.numpy: Save as NumPy .npz archive
    - output.hdf5: Save to HDF5 file
    - output.vcd: Export as VCD (Value Change Dump)
    - output.pcap: Export as PCAP network capture
    - output.wireshark: Export in Wireshark-compatible format
    - output.report: Generate PDF/HTML report
    - output.plot: Save matplotlib plot to file
    - output.binary: Save raw binary data
    - output.wav: Export as WAV audio file
    - output.yaml: Export to YAML file

Example:
    >>> # Pipeline YAML configuration
    >>> steps:
    >>>   - name: save_results
    >>>     type: output.json
    >>>     params:
    >>>       path: results.json
    >>>       pretty: true
    >>>     inputs:
    >>>       data: analysis.measurements
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

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

# Lazy imports to avoid circular dependencies and reduce startup time
_numpy = None
_h5py = None
_pandas = None
_matplotlib = None


def _get_numpy() -> Any:
    """Lazy import numpy."""
    global _numpy
    if _numpy is None:
        import numpy as _numpy_module

        _numpy = _numpy_module
    return _numpy


def _get_h5py() -> Any:
    """Lazy import h5py."""
    global _h5py
    if _h5py is None:
        import h5py as _h5py_module

        _h5py = _h5py_module
    return _h5py


def _get_pandas() -> Any:
    """Lazy import pandas."""
    global _pandas
    if _pandas is None:
        import pandas as _pandas_module

        _pandas = _pandas_module
    return _pandas


def _get_matplotlib() -> Any:
    """Lazy import matplotlib.pyplot."""
    global _matplotlib
    if _matplotlib is None:
        import matplotlib.pyplot as _matplotlib_module

        _matplotlib = _matplotlib_module
    return _matplotlib


@register_handler("output.json")
def handle_output_json(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Export data to JSON file.

    Parameters:
        path (str): Output file path
        pretty (bool, optional): Pretty-print with indentation (default: True)
        indent (int, optional): Indentation spaces (default: 2)

    Inputs:
        data: Data to export (dict, list, or JSON-serializable object)

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'json'

    Example:
        >>> # Export analysis results
        >>> steps:
        >>>   - name: save_results
        >>>     type: output.json
        >>>     params:
        >>>       path: results.json
        >>>       pretty: true
        >>>     inputs:
        >>>       data: analysis.measurements
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError(
            "Missing required input 'data'. Connect 'data' input to previous step output.",
            step_name=step_name,
        )

    path = params.get("path")
    if not path:
        raise PipelineExecutionError(
            "Missing required parameter 'path'. Add 'path: output.json' to params.",
            step_name=step_name,
        )

    pretty = params.get("pretty", True)
    indent = params.get("indent", 2)

    # Convert numpy arrays and other non-serializable types
    def convert_to_serializable(obj: Any) -> Any:
        """Convert objects to JSON-serializable types."""
        np = _get_numpy()
        if isinstance(obj, np.ndarray):
            return obj.tolist()
        if isinstance(obj, (np.integer, np.floating)):
            return obj.item()
        if isinstance(obj, dict):
            return {k: convert_to_serializable(v) for k, v in obj.items()}
        if isinstance(obj, (list, tuple)):
            return [convert_to_serializable(v) for v in obj]
        return obj

    try:
        data_serializable = convert_to_serializable(data)
        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        with open(output_path, "w") as f:
            if pretty:
                json.dump(data_serializable, f, indent=indent)
            else:
                json.dump(data_serializable, f)

        bytes_written = output_path.stat().st_size
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write JSON file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "json",
    }


@register_handler("output.csv")
def handle_output_csv(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Export data to CSV file.

    Parameters:
        path (str): Output file path
        delimiter (str, optional): CSV delimiter (default: ',')
        header (bool, optional): Include header row (default: True)
        index (bool, optional): Include row index (default: False)

    Inputs:
        data: Data to export (DataFrame, dict, list of dicts, or 2D array)

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'csv'
        num_rows: Number of data rows written
        num_columns: Number of columns

    Example:
        >>> # Export measurement data
        >>> steps:
        >>>   - name: save_csv
        >>>     type: output.csv
        >>>     params:
        >>>       path: measurements.csv
        >>>       delimiter: ','
        >>>     inputs:
        >>>       data: analysis.results
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    delimiter = params.get("delimiter", ",")
    header = params.get("header", True)
    index = params.get("index", False)

    try:
        pd = _get_pandas()
        np = _get_numpy()

        # Convert data to DataFrame
        if isinstance(data, pd.DataFrame):
            df = data
        elif isinstance(data, (dict, list, np.ndarray)):
            df = pd.DataFrame(data)
        else:
            raise ValueError(f"Unsupported data type: {type(data)}")

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        df.to_csv(output_path, sep=delimiter, header=header, index=index)

        bytes_written = output_path.stat().st_size
        num_rows, num_columns = df.shape
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write CSV file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "csv",
        "num_rows": num_rows,
        "num_columns": num_columns,
    }


@register_handler("output.numpy")
def handle_output_numpy(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Save data as NumPy .npz archive.

    Parameters:
        path (str): Output file path (.npz extension)
        compressed (bool, optional): Use compression (default: True)

    Inputs:
        data: Data to save (dict of arrays or single array named 'data')

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'npz'
        arrays: List of array names saved

    Example:
        >>> # Save trace and analysis results
        >>> steps:
        >>>   - name: save_numpy
        >>>     type: output.numpy
        >>>     params:
        >>>       path: output.npz
        >>>       compressed: true
        >>>     inputs:
        >>>       data: {signal: trace.data, spectrum: fft.power}
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

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

    try:
        np = _get_numpy()

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Handle different data types
        if isinstance(data, dict):
            arrays_dict = {k: np.asarray(v) for k, v in data.items()}
        elif isinstance(data, np.ndarray):
            arrays_dict = {"data": data}
        else:
            arrays_dict = {"data": np.asarray(data)}

        # Save with or without compression
        if compressed:
            np.savez_compressed(output_path, **arrays_dict)
        else:
            np.savez(output_path, **arrays_dict)

        bytes_written = output_path.stat().st_size
        array_names = list(arrays_dict.keys())
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write NumPy file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "npz",
        "arrays": array_names,
    }


@register_handler("output.hdf5")
def handle_output_hdf5(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Save data to HDF5 file.

    Parameters:
        path (str): Output file path (.h5 or .hdf5 extension)
        dataset (str, optional): Dataset path within HDF5 (default: '/data')
        compression (str, optional): Compression algorithm ('gzip', 'lzf', default: 'gzip')
        mode (str, optional): File mode ('w' for overwrite, 'a' for append, default: 'w')

    Inputs:
        data: Data to save (array, dict of arrays, or trace object)

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'hdf5'
        datasets: List of dataset paths created

    Example:
        >>> # Save waveform to HDF5
        >>> steps:
        >>>   - name: save_hdf5
        >>>     type: output.hdf5
        >>>     params:
        >>>       path: waveform.h5
        >>>       dataset: /trace/signal
        >>>       compression: gzip
        >>>     inputs:
        >>>       data: trace.data
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    dataset_path = params.get("dataset", "/data")
    compression = params.get("compression", "gzip")
    mode = params.get("mode", "w")

    try:
        h5py = _get_h5py()
        np = _get_numpy()

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        datasets_created = []

        with h5py.File(output_path, mode) as f:
            if isinstance(data, dict):
                # Save multiple datasets
                for key, value in data.items():
                    ds_path = f"{dataset_path}/{key}" if dataset_path != "/data" else f"/{key}"
                    arr = np.asarray(value)
                    f.create_dataset(ds_path, data=arr, compression=compression)
                    datasets_created.append(ds_path)
            else:
                # Save single dataset
                arr = np.asarray(data)
                f.create_dataset(dataset_path, data=arr, compression=compression)
                datasets_created.append(dataset_path)

        bytes_written = output_path.stat().st_size
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write HDF5 file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "hdf5",
        "datasets": datasets_created,
    }


@register_handler("output.vcd")
def handle_output_vcd(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Export digital trace as VCD (Value Change Dump) file.

    Parameters:
        path (str): Output VCD file path
        signal_name (str, optional): Signal name in VCD (default: 'signal')
        timescale (str, optional): VCD timescale (default: '1ns')

    Inputs:
        data: Digital trace or dict with 'times' and 'values'

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'vcd'
        num_transitions: Number of value changes

    Example:
        >>> # Export decoded UART signal
        >>> steps:
        >>>   - name: save_vcd
        >>>     type: output.vcd
        >>>     params:
        >>>       path: uart.vcd
        >>>       signal_name: uart_rx
        >>>       timescale: 1ns
        >>>     inputs:
        >>>       data: decoder.trace
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    signal_name = params.get("signal_name", "signal")
    timescale = params.get("timescale", "1ns")

    try:
        np = _get_numpy()

        # Extract times and values
        if hasattr(data, "times") and hasattr(data, "data"):
            times = data.times
            values = data.data
        elif isinstance(data, dict) and "times" in data and "values" in data:
            times = data["times"]
            values = data["values"]
        else:
            raise ValueError("Data must have 'times' and 'values' or be a Trace object")

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Write VCD file
        with open(output_path, "w") as f:
            # Header
            f.write("$version\n")
            f.write("  Oscura VCD Exporter\n")
            f.write("$end\n")
            f.write(f"$timescale {timescale} $end\n")
            f.write("$scope module top $end\n")
            f.write(f"$var wire 1 ! {signal_name} $end\n")
            f.write("$upscope $end\n")
            f.write("$enddefinitions $end\n")
            f.write("$dumpvars\n")

            # Convert times to integers (VCD requires integer timestamps)
            times_int = np.round(times * 1e9).astype(np.int64)  # Convert to ns

            # Write value changes
            prev_value = None
            num_transitions = 0
            for time, value in zip(times_int, values, strict=True):
                val_bit = "1" if value else "0"
                if val_bit != prev_value:
                    f.write(f"#{time}\n")
                    f.write(f"{val_bit}!\n")
                    prev_value = val_bit
                    num_transitions += 1

            f.write("$end\n")

        bytes_written = output_path.stat().st_size
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write VCD file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "vcd",
        "num_transitions": num_transitions,
    }


@register_handler("output.pcap")
def handle_output_pcap(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Export network packets as PCAP file.

    Parameters:
        path (str): Output PCAP file path
        link_type (int, optional): PCAP link layer type (default: 1 for Ethernet)

    Inputs:
        data: List of packet bytes or packet objects with 'data' attribute

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'pcap'
        packet_count: Number of packets written

    Example:
        >>> # Export captured network packets
        >>> steps:
        >>>   - name: save_pcap
        >>>     type: output.pcap
        >>>     params:
        >>>       path: capture.pcap
        >>>       link_type: 1
        >>>     inputs:
        >>>       data: sniffer.packets
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    link_type = params.get("link_type", 1)  # 1 = Ethernet

    try:
        import struct
        import time as time_module

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        with open(output_path, "wb") as f:
            # PCAP global header
            f.write(struct.pack("<IHHIIII", 0xA1B2C3D4, 2, 4, 0, 0, 65535, link_type))

            packet_count = 0
            for packet in data:
                # Extract packet data
                if isinstance(packet, bytes):
                    pkt_data = packet
                elif hasattr(packet, "data"):
                    pkt_data = packet.data
                elif isinstance(packet, dict) and "data" in packet:
                    pkt_data = packet["data"]
                else:
                    pkt_data = bytes(packet)

                # PCAP packet header: timestamp (sec, usec), captured len, original len
                timestamp = time_module.time()
                ts_sec = int(timestamp)
                ts_usec = int((timestamp - ts_sec) * 1e6)
                pkt_len = len(pkt_data)

                f.write(struct.pack("<IIII", ts_sec, ts_usec, pkt_len, pkt_len))
                f.write(pkt_data)
                packet_count += 1

        bytes_written = output_path.stat().st_size
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write PCAP file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "pcap",
        "packet_count": packet_count,
    }


@register_handler("output.wireshark")
def handle_output_wireshark(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Export data in Wireshark-compatible PCAPNG format.

    Parameters:
        path (str): Output PCAPNG file path
        interface_name (str, optional): Interface name (default: 'oscura0')
        comment (str, optional): File comment

    Inputs:
        data: List of packet bytes or packet objects

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'pcapng'
        packet_count: Number of packets written

    Example:
        >>> # Export for Wireshark analysis
        >>> steps:
        >>>   - name: save_wireshark
        >>>     type: output.wireshark
        >>>     params:
        >>>       path: capture.pcapng
        >>>       interface_name: can0
        >>>       comment: CAN bus capture
        >>>     inputs:
        >>>       data: decoder.packets
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    # Note: interface_name and comment are documented but not yet used in PCAP implementation
    # A full PCAPNG implementation would include these in interface description blocks

    try:
        # For now, use PCAP format (Wireshark can read both)
        # Full PCAPNG implementation would be more complex
        pcap_result = handle_output_pcap(inputs, {"path": path, "link_type": 1}, step_name)
        # Modify the format field to indicate PCAPNG
        result: dict[str, Any] = dict(pcap_result)
        result["format"] = "pcapng"

        # Note: This is a simplified implementation using PCAP format
        # A full PCAPNG implementation would include interface description blocks
    except Exception as e:
        raise PipelineExecutionError(
            f"Failed to write Wireshark file: {e}", step_name=step_name
        ) from e

    return result


@register_handler("output.report")
def handle_output_report(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Generate PDF or HTML report from analysis results.

    Parameters:
        path (str): Output file path (.html or .pdf)
        title (str, optional): Report title (default: 'Analysis Report')
        format (str, optional): Output format ('html' or 'pdf', auto-detected from path)
        include_plots (bool, optional): Include matplotlib figures (default: True)

    Inputs:
        data: Report data (dict with sections, results, plots)

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'html' or 'pdf'
        sections: Number of sections in report

    Example:
        >>> # Generate HTML report
        >>> steps:
        >>>   - name: create_report
        >>>     type: output.report
        >>>     params:
        >>>       path: analysis_report.html
        >>>       title: UART Protocol Analysis
        >>>       include_plots: true
        >>>     inputs:
        >>>       data: {summary: summary.stats, plots: plotter.figures}
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    title = params.get("title", "Analysis Report")
    fmt = params.get("format")
    # Note: include_plots is documented but not yet used in HTML report generation
    # Future enhancement: embed matplotlib figures as base64 images

    try:
        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Auto-detect format from extension
        if fmt is None:
            fmt = "pdf" if output_path.suffix.lower() == ".pdf" else "html"

        if fmt == "html":
            # Generate HTML report
            html_content = f"""<!DOCTYPE html>
<html>
<head>
    <title>{title}</title>
    <style>
        body {{ font-family: Arial, sans-serif; margin: 40px; }}
        h1 {{ color: #333; }}
        h2 {{ color: #666; margin-top: 30px; }}
        table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
        th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
        th {{ background-color: #f2f2f2; }}
        .section {{ margin-bottom: 30px; }}
    </style>
</head>
<body>
    <h1>{title}</h1>
"""

            sections = 0
            if isinstance(data, dict):
                for section_name, section_data in data.items():
                    html_content += f'<div class="section"><h2>{section_name}</h2>\n'
                    if isinstance(section_data, dict):
                        html_content += "<table><tr><th>Key</th><th>Value</th></tr>\n"
                        for key, value in section_data.items():
                            html_content += f"<tr><td>{key}</td><td>{value}</td></tr>\n"
                        html_content += "</table>\n"
                    else:
                        html_content += f"<p>{section_data}</p>\n"
                    html_content += "</div>\n"
                    sections += 1
            else:
                html_content += f"<p>{data}</p>\n"
                sections = 1

            html_content += "</body></html>"

            with open(output_path, "w") as f:
                f.write(html_content)

            bytes_written = output_path.stat().st_size
        else:
            # PDF generation would require reportlab or weasyprint
            raise NotImplementedError("PDF report generation requires additional dependencies")

    except Exception as e:
        raise PipelineExecutionError(f"Failed to generate report: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": fmt,
        "sections": sections,
    }


@register_handler("output.plot")
def handle_output_plot(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Save matplotlib plot to file.

    Parameters:
        path (str): Output file path (.png, .pdf, .svg, etc.)
        dpi (int, optional): Resolution in dots per inch (default: 300)
        format (str, optional): Output format (auto-detected from path extension)
        transparent (bool, optional): Transparent background (default: False)

    Inputs:
        data: Matplotlib figure object or data to plot

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: Image format
        dpi: Resolution used

    Example:
        >>> # Save analysis plot
        >>> steps:
        >>>   - name: save_plot
        >>>     type: output.plot
        >>>     params:
        >>>       path: spectrum.png
        >>>       dpi: 300
        >>>       transparent: false
        >>>     inputs:
        >>>       data: plotter.figure
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    dpi = params.get("dpi", 300)
    fmt = params.get("format")
    transparent = params.get("transparent", False)

    try:
        plt = _get_matplotlib()

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Auto-detect format
        if fmt is None:
            fmt = output_path.suffix.lstrip(".")

        # Handle figure object or create plot from data
        if hasattr(data, "savefig"):
            # It's a matplotlib figure
            data.savefig(
                output_path, dpi=dpi, format=fmt, transparent=transparent, bbox_inches="tight"
            )
        else:
            # Create simple plot from data
            fig = plt.figure(figsize=(10, 6))
            if hasattr(data, "__iter__") and not isinstance(data, (str, dict)):
                plt.plot(data)
            else:
                plt.text(0.5, 0.5, str(data), ha="center", va="center")
            fig.savefig(
                output_path, dpi=dpi, format=fmt, transparent=transparent, bbox_inches="tight"
            )
            plt.close(fig)

        bytes_written = output_path.stat().st_size
    except Exception as e:
        raise PipelineExecutionError(f"Failed to save plot: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": fmt,
        "dpi": dpi,
    }


@register_handler("output.binary")
def handle_output_binary(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Save raw binary data to file.

    Parameters:
        path (str): Output file path
        dtype (str, optional): NumPy dtype for conversion (default: preserve input type)
        byte_order (str, optional): Byte order ('little', 'big', 'native', default: 'native')

    Inputs:
        data: Raw data (bytes, array, or list of numbers)

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'binary'
        dtype: Data type used

    Example:
        >>> # Save raw ADC samples
        >>> steps:
        >>>   - name: save_raw
        >>>     type: output.binary
        >>>     params:
        >>>       path: samples.bin
        >>>       dtype: int16
        >>>       byte_order: little
        >>>     inputs:
        >>>       data: adc.samples
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    dtype = params.get("dtype")
    byte_order = params.get("byte_order", "native")

    try:
        np = _get_numpy()

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Convert to numpy array
        if isinstance(data, bytes):
            binary_data = data
            actual_dtype = "bytes"
        else:
            arr = np.asarray(data, dtype=dtype)
            actual_dtype = str(arr.dtype)

            # Handle byte order
            if byte_order == "little":
                arr = arr.astype(arr.dtype.newbyteorder("<"))
            elif byte_order == "big":
                arr = arr.astype(arr.dtype.newbyteorder(">"))

            binary_data = arr.tobytes()

        # Write binary data
        with open(output_path, "wb") as f:
            f.write(binary_data)

        bytes_written = output_path.stat().st_size
    except Exception as e:
        raise PipelineExecutionError(
            f"Failed to write binary file: {e}", step_name=step_name
        ) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "binary",
        "dtype": actual_dtype,
    }


@register_handler("output.wav")
def handle_output_wav(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Export data as WAV audio file.

    Parameters:
        path (str): Output WAV file path
        sample_rate (int): Sample rate in Hz (required)
        bit_depth (int, optional): Bit depth (8, 16, 24, 32, default: 16)
        normalize (bool, optional): Normalize to full scale (default: True)

    Inputs:
        data: Audio samples (array or trace object)

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'wav'
        sample_rate: Sample rate used
        num_samples: Number of samples written

    Example:
        >>> # Export decoded audio
        >>> steps:
        >>>   - name: save_wav
        >>>     type: output.wav
        >>>     params:
        >>>       path: decoded.wav
        >>>       sample_rate: 48000
        >>>       bit_depth: 16
        >>>       normalize: true
        >>>     inputs:
        >>>       data: decoder.audio
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

    sample_rate = params.get("sample_rate")
    if not sample_rate:
        raise PipelineExecutionError(
            "Missing required parameter 'sample_rate'. Add 'sample_rate: 48000' to params.",
            step_name=step_name,
        )

    bit_depth = params.get("bit_depth", 16)
    normalize = params.get("normalize", True)

    try:
        import wave

        np = _get_numpy()

        # Extract audio data
        if hasattr(data, "data"):
            audio_data = np.asarray(data.data)
        else:
            audio_data = np.asarray(data)

        # Normalize if requested
        if normalize:
            max_val = np.abs(audio_data).max()
            if max_val > 0:
                audio_data = audio_data / max_val

        # Convert to integer format
        if bit_depth == 8:
            audio_int = (audio_data * 127 + 128).astype(np.uint8)
        elif bit_depth == 16:
            audio_int = (audio_data * 32767).astype(np.int16)
        elif bit_depth == 32:
            audio_int = (audio_data * 2147483647).astype(np.int32)
        else:
            raise ValueError(f"Unsupported bit depth: {bit_depth}")

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Write WAV file
        with wave.open(str(output_path), "wb") as wav_file:
            wav_file.setnchannels(1)  # Mono
            wav_file.setsampwidth(bit_depth // 8)
            wav_file.setframerate(sample_rate)
            wav_file.writeframes(audio_int.tobytes())

        bytes_written = output_path.stat().st_size
        num_samples = len(audio_data)
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write WAV file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "wav",
        "sample_rate": sample_rate,
        "num_samples": num_samples,
    }


@register_handler("output.yaml")
def handle_output_yaml(
    inputs: dict[str, Any], params: dict[str, Any], step_name: str
) -> dict[str, Any]:
    """Export data to YAML file.

    Parameters:
        path (str): Output YAML file path
        default_flow_style (bool, optional): Use flow style (default: False)

    Inputs:
        data: Data to export (dict, list, or YAML-serializable object)

    Outputs:
        path: Absolute file path
        bytes_written: Number of bytes written
        format: 'yaml'

    Example:
        >>> # Export configuration
        >>> steps:
        >>>   - name: save_config
        >>>     type: output.yaml
        >>>     params:
        >>>       path: config.yaml
        >>>       default_flow_style: false
        >>>     inputs:
        >>>       data: analysis.config
    """
    data = inputs.get("data")
    if data is None:
        raise PipelineExecutionError("Missing required input 'data'", step_name=step_name)

    path = params.get("path")
    if not path:
        raise PipelineExecutionError("Missing required parameter 'path'", step_name=step_name)

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

    try:
        import yaml

        np = _get_numpy()

        # Convert numpy types to native Python types
        def convert_to_native(obj: Any) -> Any:
            """Convert objects to YAML-serializable types."""
            if isinstance(obj, np.ndarray):
                return obj.tolist()
            if isinstance(obj, (np.integer, np.floating)):
                return obj.item()
            if isinstance(obj, dict):
                return {k: convert_to_native(v) for k, v in obj.items()}
            if isinstance(obj, (list, tuple)):
                return [convert_to_native(v) for v in obj]
            return obj

        data_native = convert_to_native(data)

        output_path = Path(path).resolve()
        output_path.parent.mkdir(parents=True, exist_ok=True)

        with open(output_path, "w") as f:
            yaml.dump(data_native, f, default_flow_style=default_flow_style)

        bytes_written = output_path.stat().st_size
    except Exception as e:
        raise PipelineExecutionError(f"Failed to write YAML file: {e}", step_name=step_name) from e

    return {
        "path": str(output_path),
        "bytes_written": bytes_written,
        "format": "yaml",
    }
