"""DBC file generator with intelligent signal detection and naming.

This module provides enhanced DBC generation from CAN reverse engineering,
featuring:
- Correlation-based multi-bit signal detection
- Intelligent signal naming from stimulus labels and patterns
- Automatic endianness detection for multi-byte signals

The generator analyzes bit-level correlations to identify signal boundaries,
applies pattern recognition for intelligent naming, and automatically detects
the correct byte order for multi-byte signals.
"""

from __future__ import annotations

import re
from collections import Counter
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar

import numpy as np
from numpy.typing import NDArray
from scipy.stats import pearsonr

if TYPE_CHECKING:
    from oscura.automotive.can.discovery import DiscoveryDocument
    from oscura.automotive.can.session import CANSession

from oscura.automotive.can.dbc_generator import (
    DBCGenerator as _DBCGeneratorImpl,
)
from oscura.automotive.can.dbc_generator import (
    DBCMessage,
    DBCSignal,
)

__all__ = ["DBCGenerator", "SignalDetector", "SignalNamer"]


class SignalDetector:
    """Detect signal boundaries using bit-level correlation analysis.

    This class analyzes bit-level patterns across multiple CAN messages to identify
    multi-bit signals that change together. Correlation drops indicate signal boundaries.

    Example:
        >>> detector = SignalDetector()
        >>> data = [b'\\x00\\x10', b'\\x00\\x20', b'\\x00\\x30']
        >>> signals = detector.detect_correlated_bits(data)
        >>> # Returns [(8, 8, 'big_endian')] for changing second byte
    """

    @staticmethod
    def detect_correlated_bits(
        data_bytes: list[bytes], min_correlation: float = 0.85
    ) -> list[tuple[int, int, str]]:
        """Detect multi-bit signals using correlation analysis.

        Analyzes bit-level correlation across messages to identify bits that change
        together as a group. When correlation drops between adjacent bits, a signal
        boundary is detected.

        Args:
            data_bytes: List of CAN message data bytes (multiple messages).
            min_correlation: Minimum correlation threshold (0.0-1.0). Higher values
                require stronger correlation to group bits together.

        Returns:
            List of (start_bit, bit_length, byte_order) tuples representing detected
            signals. byte_order is 'little_endian' or 'big_endian'.

        Example:
            >>> detector = SignalDetector()
            >>> # Messages with 16-bit counter at byte 1-2
            >>> data = [b'\\x00\\x00\\x10', b'\\x00\\x00\\x20', b'\\x00\\x00\\x30']
            >>> signals = detector.detect_correlated_bits(data)
            >>> # Returns [(8, 16, 'big_endian')] for the counter signal
        """
        if len(data_bytes) < 2:
            return []

        # Determine max byte length across all messages
        max_len = max(len(d) for d in data_bytes)
        if max_len == 0:
            return []

        # Convert to bit arrays (pad shorter messages to max length)
        bit_arrays = []
        for data in data_bytes:
            padded = data + b"\x00" * (max_len - len(data))
            bits = np.unpackbits(np.frombuffer(padded, dtype=np.uint8))
            bit_arrays.append(bits)

        bit_matrix = np.array(bit_arrays)

        # Need at least 2 messages for correlation
        if bit_matrix.shape[0] < 2:
            return []

        signals = []
        visited = set()

        # Scan each bit position for potential signal starts
        for start_bit in range(max_len * 8):
            if start_bit in visited:
                continue

            # Skip constant bits (no variance = no information)
            bit_column = bit_matrix[:, start_bit]
            if np.std(bit_column) == 0:
                visited.add(start_bit)
                continue

            # Find correlated adjacent bits by comparing each successive bit
            correlated_bits = [start_bit]
            current_bit = start_bit

            while current_bit + 1 < max_len * 8:
                next_bit = current_bit + 1
                next_column = bit_matrix[:, next_bit]

                # Skip constant bits (signal boundary detected)
                if np.std(next_column) == 0:
                    break

                # Calculate Pearson correlation between current and next bit
                try:
                    correlation, _ = pearsonr(bit_column, next_column)
                except Exception:
                    correlation = 0.0

                # High correlation means bits change together (same signal)
                if abs(correlation) >= min_correlation:
                    correlated_bits.append(next_bit)
                    current_bit = next_bit
                else:
                    # Correlation dropped - signal boundary detected
                    break

            # Create signal from correlated bits
            if len(correlated_bits) >= 1:
                bit_length = len(correlated_bits)
                visited.update(correlated_bits)

                # Detect endianness for multi-byte signals
                byte_order = SignalDetector._detect_byte_order(
                    bit_matrix, start_bit, bit_length, data_bytes
                )

                signals.append((start_bit, bit_length, byte_order))

        return signals

    @staticmethod
    def _detect_byte_order(
        bit_matrix: NDArray[np.uint8], start_bit: int, bit_length: int, data_bytes: list[bytes]
    ) -> str:
        """Detect byte order (endianness) for multi-byte signal.

        Compares big-endian vs little-endian interpretations of the signal data
        to determine which byte order produces more realistic value progressions.
        Automotive CAN typically uses big-endian (Motorola) byte order.

        Args:
            bit_matrix: Matrix of bit values across messages (unused but kept for API).
            start_bit: Signal start bit position.
            bit_length: Signal length in bits.
            data_bytes: Original message data bytes.

        Returns:
            'little_endian' or 'big_endian'.
        """
        # Single-byte signals default to little endian (no byte order issues)
        if bit_length <= 8:
            return "little_endian"

        # Multi-byte signals: extract and compare both byte orders
        start_byte = start_bit // 8
        num_bytes = (bit_length + 7) // 8

        # Extract values in both byte orders
        big_endian_values = []
        little_endian_values = []

        for data in data_bytes:
            if len(data) < start_byte + num_bytes:
                continue

            byte_slice = data[start_byte : start_byte + num_bytes]

            # Big-endian (most significant byte first - typical for automotive)
            big_val = int.from_bytes(byte_slice, byteorder="big")
            big_endian_values.append(big_val)

            # Little-endian (least significant byte first)
            little_val = int.from_bytes(byte_slice, byteorder="little")
            little_endian_values.append(little_val)

        if not big_endian_values:
            return "big_endian"  # Default for automotive CAN

        # Heuristic 1: Variance of differences (smoother = correct)
        # Automotive signals typically change gradually, not with huge jumps
        big_diffs = np.diff(big_endian_values) if len(big_endian_values) > 1 else [0]
        little_diffs = np.diff(little_endian_values) if len(little_endian_values) > 1 else [0]

        big_variance = np.var(big_diffs)
        little_variance = np.var(little_diffs)

        # Heuristic 2: Realistic value ranges
        # Byte-swapped values often have unrealistic magnitudes
        little_max = max(little_endian_values)

        # Prefer big-endian (automotive industry standard)
        # Only use little-endian if significantly smoother AND reasonable range
        if little_variance < big_variance * 0.5 and little_max < 100000:
            return "little_endian"

        return "big_endian"

    @staticmethod
    def analyze_stimulus_correlation(
        baseline_data: list[bytes],
        stimulus_data: list[bytes],
        stimulus_label: str,
    ) -> list[tuple[int, int, str, str]]:
        """Detect signals that correlate with stimulus events.

        Compares baseline (no action) vs stimulus (action performed) captures to identify
        which bits change in response to the stimulus. This enables rapid signal mapping
        during reverse engineering.

        Args:
            baseline_data: Baseline message data (no stimulus).
            stimulus_data: Stimulus message data (during/after stimulus).
            stimulus_label: Stimulus description (e.g., "rpm_increase", "brake_pressed").

        Returns:
            List of (start_bit, bit_length, byte_order, stimulus_hint) tuples for signals
            that changed significantly during stimulus.

        Example:
            >>> detector = SignalDetector()
            >>> baseline = [b'\\x00\\x00\\x10', b'\\x00\\x00\\x10']
            >>> stimulus = [b'\\x00\\x00\\x20', b'\\x00\\x00\\x30']
            >>> signals = detector.analyze_stimulus_correlation(
            ...     baseline, stimulus, "rpm_increase"
            ... )
            >>> # Returns [(8, 16, 'big_endian', 'rpm_increase')]
        """
        if len(baseline_data) < 2 or len(stimulus_data) < 2:
            return []

        # Detect signals in stimulus dataset
        stimulus_signals = SignalDetector.detect_correlated_bits(stimulus_data)

        # Find signals that changed between baseline and stimulus
        changed_signals = []

        for start_bit, bit_length, byte_order in stimulus_signals:
            # Extract values for this signal in both datasets
            baseline_values = SignalDetector._extract_signal_values(
                baseline_data, start_bit, bit_length
            )
            stimulus_values = SignalDetector._extract_signal_values(
                stimulus_data, start_bit, bit_length
            )

            # Check if signal changed significantly
            if SignalDetector._signal_changed_significantly(baseline_values, stimulus_values):
                changed_signals.append((start_bit, bit_length, byte_order, stimulus_label))

        return changed_signals

    @staticmethod
    def _extract_signal_values(
        data_bytes: list[bytes], start_bit: int, bit_length: int
    ) -> list[int]:
        """Extract signal values from message data.

        Args:
            data_bytes: Message data bytes.
            start_bit: Signal start bit.
            bit_length: Signal bit length.

        Returns:
            List of extracted signal values.
        """
        values = []
        start_byte = start_bit // 8
        num_bytes = (bit_length + 7) // 8

        for data in data_bytes:
            if len(data) < start_byte + num_bytes:
                continue

            byte_slice = data[start_byte : start_byte + num_bytes]
            value = int.from_bytes(byte_slice, byteorder="big")
            values.append(value)

        return values

    @staticmethod
    def _signal_changed_significantly(baseline: list[int], stimulus: list[int]) -> bool:
        """Check if signal changed significantly between baseline and stimulus.

        Args:
            baseline: Baseline signal values.
            stimulus: Stimulus signal values.

        Returns:
            True if signal changed significantly.
        """
        if not baseline or not stimulus:
            return False

        # Check mean difference
        baseline_mean = np.mean(baseline)
        stimulus_mean = np.mean(stimulus)

        # Check variance difference
        baseline_std = np.std(baseline)
        stimulus_std = np.std(stimulus)

        # Signal changed if mean or variance changed significantly
        mean_change = abs(stimulus_mean - baseline_mean)
        variance_change = abs(stimulus_std - baseline_std)

        # Thresholds: >10% mean change or >50% variance increase
        if baseline_mean > 0:
            mean_change_pct = mean_change / baseline_mean
            if mean_change_pct > 0.1:
                return True

        return bool(variance_change > baseline_std * 0.5)


class SignalNamer:
    """Generate intelligent signal names from stimulus labels and patterns.

    This class applies pattern recognition and heuristics to generate meaningful
    signal names from stimulus labels (e.g., "rpm_increase" → "EngineRPM") and
    value patterns (e.g., sequential counter → "MsgCounter").

    Example:
        >>> namer = SignalNamer()
        >>> namer.name_from_stimulus("rpm_increase", (0, 8000))
        'EngineRPM'
        >>> namer.name_from_pattern([0, 1, 2, 3, 4], 8)
        'MsgCounter'
    """

    # Pattern-based name mappings (automotive domain knowledge)
    STIMULUS_PATTERNS: ClassVar[dict[str, str]] = {
        r"rpm|engine.*speed": "EngineRPM",
        r"throttle|accel|pedal": "ThrottlePosition",
        r"brake": "BrakeStatus",
        r"speed|velocity": "VehicleSpeed",
        r"steer|wheel": "SteeringAngle",
        r"temp|temperature": "Temperature",
        r"voltage|volt": "Voltage",
        r"current|amp": "Current",
        r"pressure": "Pressure",
        r"gear": "GearPosition",
        r"torque": "Torque",
        r"fuel": "FuelLevel",
        r"battery": "BatteryVoltage",
        r"coolant": "CoolantTemp",
        r"oil": "OilPressure",
        r"wheel.*speed": "WheelSpeed",
        r"door": "DoorStatus",
        r"window": "WindowPosition",
        r"seat": "SeatPosition",
        r"mirror": "MirrorPosition",
        r"wiper": "WiperStatus",
        r"light|lamp": "LightStatus",
        r"turn.*signal": "TurnSignal",
        r"horn": "HornStatus",
        r"airbag": "AirbagStatus",
        r"abs": "ABSStatus",
        r"traction": "TractionControl",
        r"cruise": "CruiseControl",
    }

    # Value range-based suffixes
    VALUE_RANGE_SUFFIXES: ClassVar[dict[tuple[int, int], str]] = {
        (0, 100): "_pct",
        (0, 360): "_deg",
        (-180, 180): "_deg",
        (0, 1): "_flag",
    }

    @staticmethod
    def name_from_stimulus(
        stimulus_label: str,
        value_range: tuple[float, float] | None = None,
        is_signed: bool = False,
    ) -> str:
        """Generate signal name from stimulus label.

        Extracts meaningful signal names from user actions during reverse engineering.
        For example, "rpm_increase" becomes "EngineRPM" using pattern matching.

        Args:
            stimulus_label: Stimulus label (e.g., "rpm_increase", "brake_pressed").
            value_range: Observed (min, max) value range for suffix hints.
            is_signed: Whether signal is signed (adds "_signed" suffix if needed).

        Returns:
            Signal name in PascalCase with appropriate suffix.

        Example:
            >>> namer = SignalNamer()
            >>> namer.name_from_stimulus("rpm_increase", (0, 8000))
            'EngineRPM'
            >>> namer.name_from_stimulus("throttle_50pct", (0, 100))
            'ThrottlePosition_pct'
            >>> namer.name_from_stimulus("brake_pressed")
            'BrakeStatus'
        """
        # Clean stimulus label (remove punctuation, convert to lowercase)
        label_lower = stimulus_label.lower()
        label_lower = re.sub(r"[_\-\.]", " ", label_lower)

        # Try pattern matching against known automotive terms
        for pattern, name in SignalNamer.STIMULUS_PATTERNS.items():
            if re.search(pattern, label_lower):
                base_name = name
                break
        else:
            # No pattern match - extract meaningful words
            words = label_lower.split()
            # Remove common action words that don't indicate signal meaning
            filtered = [
                w
                for w in words
                if w
                not in {
                    "increase",
                    "decrease",
                    "pressed",
                    "released",
                    "on",
                    "off",
                    "activate",
                    "deactivate",
                    "set",
                    "get",
                }
            ]
            if filtered:
                base_name = "".join(w.capitalize() for w in filtered)
            else:
                base_name = "Signal"

        # Add range-based suffix for additional context
        suffix = ""
        if value_range:
            min_val, max_val = value_range
            for (range_min, range_max), range_suffix in SignalNamer.VALUE_RANGE_SUFFIXES.items():
                if range_min <= min_val <= max_val <= range_max:
                    suffix = range_suffix
                    break

        # Add signed suffix if needed (and no other suffix)
        if is_signed and not suffix:
            suffix = "_signed"

        return base_name + suffix

    @staticmethod
    def name_from_pattern(
        byte_values: list[int],
        bit_length: int,
        is_signed: bool = False,
    ) -> str:
        """Generate signal name from value pattern analysis.

        Analyzes the observed values to detect common patterns like counters,
        checksums, flags, and enumerations.

        Args:
            byte_values: Observed signal values across multiple messages.
            bit_length: Signal length in bits.
            is_signed: Whether signal is signed.

        Returns:
            Signal name based on detected pattern (e.g., "MsgCounter", "Checksum").

        Example:
            >>> namer = SignalNamer()
            >>> namer.name_from_pattern([0, 1, 2, 3, 4, 5], 8)
            'MsgCounter'
            >>> namer.name_from_pattern([0, 0, 0, 1, 1, 1], 1)
            'StatusFlag'
            >>> namer.name_from_pattern([0x3A, 0x7F, 0x92, 0x1B], 8)
            'Checksum'
        """
        if not byte_values:
            return "UnknownSignal"

        unique_values = set(byte_values)
        value_count = len(unique_values)

        # Pattern 1: Counter (sequential incrementing values)
        if value_count > 4 and len(byte_values) > 1:
            diffs = [byte_values[i + 1] - byte_values[i] for i in range(len(byte_values) - 1)]
            if diffs:
                common_diff = Counter(diffs).most_common(1)[0][0]
                # Rolling counter with increment of 1
                if common_diff == 1 and value_count > len(byte_values) * 0.5:
                    return "MsgCounter"
                # Frame counter with other increment
                elif common_diff != 0:
                    return "FrameCounter"

        # Pattern 2: Boolean/flag (only 0 and 1)
        if unique_values <= {0, 1} and bit_length == 1:
            return "StatusFlag"

        # Pattern 3: Checksum (high entropy, covers most of value range)
        max_possible = (1 << bit_length) - 1
        if value_count > max_possible * 0.75:
            return "Checksum"

        # Pattern 4: Enum (few discrete values)
        if value_count <= 8:
            if bit_length <= 4:
                return "StatusCode"
            else:
                return "ModeSelect"

        # Pattern 5: Value signal (continuous range analysis)
        min_val, max_val = min(byte_values), max(byte_values)

        # Percentage-like (0-100 range)
        if min_val >= 0 and max_val <= 100:
            return "Value_pct"

        # Angle-like (0-360 range)
        if min_val >= 0 and max_val <= 360:
            return "Angle_deg"

        # Temperature-like (signed, reasonable range for °C)
        if is_signed and -40 <= min_val <= 150:
            return "Temperature"

        # Generic data signal (no pattern detected)
        return "DataSignal"

    @staticmethod
    def generate_name(
        start_bit: int,
        bit_length: int,
        byte_values: list[int] | None = None,
        stimulus_label: str | None = None,
        value_range: tuple[float, float] | None = None,
        is_signed: bool = False,
    ) -> str:
        """Generate signal name using all available heuristics.

        Combines stimulus-based, pattern-based, and position-based naming strategies
        to generate the most descriptive possible signal name.

        Args:
            start_bit: Signal start bit position.
            bit_length: Signal length in bits.
            byte_values: Observed values (optional).
            stimulus_label: Stimulus label if available (optional).
            value_range: Observed value range (optional).
            is_signed: Whether signal is signed.

        Returns:
            Best possible signal name based on available information.

        Example:
            >>> namer = SignalNamer()
            >>> # Stimulus-based naming (most preferred)
            >>> namer.generate_name(0, 16, stimulus_label="rpm_increase")
            'EngineRPM'
            >>> # Pattern-based naming (fallback)
            >>> namer.generate_name(0, 8, byte_values=[0, 1, 2, 3])
            'MsgCounter'
            >>> # Position-based naming (last resort)
            >>> namer.generate_name(16, 8)
            'Signal_B2_b0_8bit'
        """
        # Strategy 1: Stimulus-based naming (highest confidence)
        if stimulus_label:
            return SignalNamer.name_from_stimulus(stimulus_label, value_range, is_signed)

        # Strategy 2: Pattern-based naming (medium confidence)
        if byte_values:
            return SignalNamer.name_from_pattern(byte_values, bit_length, is_signed)

        # Strategy 3: Position-based naming (last resort)
        byte_pos = start_bit // 8
        bit_offset = start_bit % 8
        return f"Signal_B{byte_pos}_b{bit_offset}_{bit_length}bit"


class DBCGenerator:
    """Enhanced DBC generator with intelligent signal detection and naming.

    This class provides advanced DBC generation featuring:
    - Correlation-based signal boundary detection
    - Intelligent signal naming from stimulus labels
    - Automatic endianness detection

    The generator can work with discovery documents or raw CAN sessions to produce
    high-quality DBC files with meaningful signal names and correct byte orders.

    Example:
        >>> from oscura.automotive.can.discovery import DiscoveryDocument
        >>> doc = DiscoveryDocument()
        >>> # ... add messages and signals ...
        >>> DBCGenerator.generate(doc, "output.dbc")
    """

    @staticmethod
    def generate(
        discovery: DiscoveryDocument,
        output_path: Path | str,
        min_confidence: float = 0.0,
        include_comments: bool = True,
        enable_correlation_detection: bool = True,
        enable_intelligent_naming: bool = True,
    ) -> None:
        """Generate DBC file from discovery document with enhancements.

        Args:
            discovery: DiscoveryDocument with discovered signals.
            output_path: Output DBC file path.
            min_confidence: Minimum confidence threshold for including signals (0.0-1.0).
            include_comments: Include evidence as comments in DBC file.
            enable_correlation_detection: Use correlation-based signal detection (reserved).
            enable_intelligent_naming: Use intelligent signal naming.

        Example:
            >>> from oscura.automotive.can.discovery import DiscoveryDocument
            >>> doc = DiscoveryDocument()
            >>> # ... add messages and signals ...
            >>> DBCGenerator.generate(
            ...     doc,
            ...     "output.dbc",
            ...     min_confidence=0.8,
            ...     enable_intelligent_naming=True
            ... )
        """
        path = Path(output_path)

        # Create instance of underlying DBC generator implementation
        gen = _DBCGeneratorImpl()

        # Convert discovery document to DBC format
        for msg_id, msg_discovery in sorted(discovery.messages.items()):
            # Filter signals by confidence threshold
            signals = [s for s in msg_discovery.signals if s.confidence >= min_confidence]

            if not signals:
                continue  # Skip messages with no high-confidence signals

            # Convert signals to DBC format
            dbc_signals = []
            for sig in signals:
                # Determine value type (unsigned/signed)
                value_type: str = sig.value_type
                if value_type not in ("unsigned", "signed"):
                    value_type = "unsigned"

                # Apply intelligent naming if enabled
                signal_name = sig.name
                if enable_intelligent_naming:
                    # Extract stimulus hint from evidence
                    stimulus_hint = None
                    for evidence in sig.evidence:
                        if "stimulus:" in evidence.lower():
                            stimulus_hint = evidence.split(":", 1)[1].strip()
                            break

                    # Generate better name using all available information
                    value_range = None
                    if sig.min_value is not None and sig.max_value is not None:
                        value_range = (sig.min_value, sig.max_value)

                    improved_name = SignalNamer.generate_name(
                        start_bit=sig.start_bit,
                        bit_length=sig.length,
                        stimulus_label=stimulus_hint,
                        value_range=value_range,
                        is_signed=(value_type == "signed"),
                    )

                    # Use improved name if original is generic
                    if signal_name.startswith(("Signal_", "Byte_", "Unknown")):
                        signal_name = improved_name

                # Create DBC signal
                dbc_sig = DBCSignal(
                    name=signal_name,
                    start_bit=sig.start_bit,
                    bit_length=sig.length,
                    byte_order=sig.byte_order,
                    value_type=value_type,  # type: ignore[arg-type]
                    factor=sig.scale,
                    offset=sig.offset,
                    min_value=sig.min_value if sig.min_value is not None else 0.0,
                    max_value=sig.max_value if sig.max_value is not None else 0.0,
                    unit=sig.unit,
                    receivers=["Vector__XXX"],
                    comment="; ".join(sig.evidence) if include_comments and sig.evidence else "",
                )
                dbc_signals.append(dbc_sig)

            # Create DBC message
            dbc_msg = DBCMessage(
                message_id=msg_id,
                name=msg_discovery.name,
                dlc=msg_discovery.length,
                sender=msg_discovery.transmitter if msg_discovery.transmitter else "Vector__XXX",
                signals=dbc_signals,
                comment=msg_discovery.comment if msg_discovery.comment else "",
                cycle_time=int(msg_discovery.cycle_time_ms)
                if msg_discovery.cycle_time_ms is not None
                else None,
                send_type="Cyclic",
            )

            gen.add_message(dbc_msg)

        # Generate DBC file
        gen.generate(path)

    @staticmethod
    def generate_from_session(
        session: CANSession,
        output_path: Path | str,
        min_confidence: float = 0.8,
    ) -> None:
        """Generate DBC file from CANSession with documented signals.

        Args:
            session: CANSession with documented signals.
            output_path: Output DBC file path.
            min_confidence: Minimum confidence threshold (reserved for future use).

        Example:
            >>> from oscura.automotive.can.session import CANSession
            >>> session = CANSession.from_file("capture.blf")
            >>> # ... document signals ...
            >>> DBCGenerator.generate_from_session(session, "output.dbc")
        """
        from oscura.automotive.can.discovery import (
            DiscoveryDocument,
            MessageDiscovery,
            SignalDiscovery,
        )

        # Build discovery document from session
        doc = DiscoveryDocument()

        # Get all unique IDs that have documented signals
        for arb_id in session.unique_ids():
            try:
                msg_wrapper = session.message(arb_id)
                documented = msg_wrapper.get_documented_signals()

                if documented:
                    # Create message discovery
                    analysis = session.analyze_message(arb_id)

                    signal_discoveries = []
                    for sig_def in documented.values():
                        sig_disc = SignalDiscovery.from_definition(
                            sig_def,
                            confidence=1.0,
                            evidence=[],
                        )
                        signal_discoveries.append(sig_disc)

                    msg_disc = MessageDiscovery(
                        id=arb_id,
                        name=f"Message_{arb_id:03X}",
                        length=max(
                            msg.dlc for msg in session._messages.filter_by_id(arb_id).messages
                        ),
                        cycle_time_ms=analysis.period_ms,
                        confidence=1.0,
                        signals=signal_discoveries,
                    )

                    doc.add_message(msg_disc)

            except Exception:
                # Skip messages without documented signals
                pass

        # Generate DBC using main method
        DBCGenerator.generate(doc, output_path, min_confidence=0.0)
