#!/usr/bin/env python3
"""
UART Core - vzug-e-hinge
=========================
Clean UART port management with packet detection and timestamping.

Features:
- Single port per instance (no multi-port management)
- Reader thread writes to circular buffer
- Start/stop condition detection with timestamps
- Packet counting per connection
- Configurable stop conditions (timeout or terminator byte)
- Polling mode with grace period
- **NEW: Packet detection in listening mode with real-time timestamps**
- Buffer overflow tracking
- Clean connect/disconnect (no auto-reconnect)

Author: Kynsight
Version: 2.1.0 - Added packet detection for listening mode
"""

from __future__ import annotations
import threading
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, Callable, Tuple

try:
    import serial
except ImportError:
    serial = None

from buffer_kit.circular_buffer import (
    Status as BufferStatus,
    CircularBufferHandle,
    cb_init,
    cb_write,
    cb_copy_span,
    cb_w_abs,
    cb_capacity,
    cb_fill_bytes,
    cb_fill_pct,
    cb_overflows,
)


__all__ = [
    # Status & Configuration
    'Status',
    'StopConditionMode',
    'UARTConfig',
    'PacketConfig',
    'PacketInfo',
    'PortStatus',
    'UARTPort',
    
    # Lifecycle
    'uart_create',
    'uart_open',
    'uart_close',
    
    # Reader thread
    'uart_start_reader',
    'uart_stop_reader',
    
    # Write
    'uart_write',
    
    # Packet detection modes
    'uart_send_and_receive',
    'uart_send_and_read_pgkomm2',
    'uart_poll_packet',
    
    # Listening mode
    'uart_start_listening',
    'uart_stop_listening',
    'uart_read_buffer',
    
    # NEW: Listening with packet detection
    'uart_start_listening_with_packets',
    'uart_get_detected_packets',
    'uart_get_packet_errors',
    'uart_clear_detected_packets',

    # Status
    'uart_get_status',
]


# =============================================================================
# Status & Configuration
# =============================================================================

class Status(Enum):
    """Operation status codes."""
    OK = 0
    TIMEOUT = 1
    TIMEOUT_NO_DATA = 2
    BUFFER_OVERFLOW = 3
    IO_ERROR = 4
    BAD_CONFIG = 5
    PORT_CLOSED = 6
    ALREADY_OPEN = 7
    READER_NOT_RUNNING = 8


class StopConditionMode(Enum):
    """How to detect packet end."""
    TIMEOUT = 0      # No data for N ms
    TERMINATOR = 1   # Specific byte received


@dataclass(frozen=True)
class UARTConfig:
    """
    UART port configuration.
    
    device: Serial device path (e.g., "/dev/ttyUSB0")
    baudrate: Baud rate (e.g., 115200)
    data_bits: Data bits (7 or 8)
    stop_bits: Stop bits (1 or 2)
    parity: Parity ('N', 'E', 'O', 'M', 'S')
    buffer_size: RX circular buffer capacity in bytes
    read_chunk_size: Max bytes to read per loop iteration
    
    stop_mode: TIMEOUT or TERMINATOR
    stop_timeout_ms: Timeout in ms (for TIMEOUT mode or grace period)
    stop_terminator: Byte to detect (for TERMINATOR mode)
    
    polling_mode: If True, enables grace period before timeout
    grace_timeout_ms: Max wait for first byte in polling mode
    
    timestamp_source: Optional external time function (default: time.perf_counter)
    """
    device: str
    baudrate: int
    data_bits: int = 8
    stop_bits: int = 1
    parity: str = 'E'
    buffer_size: int = 0.256 * 1024 * 1024  # 4MB default buffer
    read_chunk_size: int = 512
    
    stop_mode: StopConditionMode = StopConditionMode.TIMEOUT
    stop_timeout_ms: int = 150
    stop_terminator: int = 0x0A  # Newline
    
    polling_mode: bool = False
    grace_timeout_ms: int = 150
    
    timestamp_source: Optional[Callable[[], float]] = None


@dataclass(frozen=True)
class PacketConfig:
    """
    Packet detection configuration for listening mode.
    
    Used to detect packet boundaries in continuous data stream.
    
    Example for format: EF FE [14 bytes] EE
        PacketConfig(
            enable=True,
            start_marker=b'\\xEF\\xFE',
            packet_length=17,
            end_marker=b'\\xEE',
            on_packet_callback=my_callback_function
        )
    
    Fields:
        enable: Enable/disable packet detection
        start_marker: Bytes that mark packet start (e.g., b'\\xEF\\xFE')
        packet_length: Total packet length in bytes (including markers)
        end_marker: Bytes that mark packet end (e.g., b'\\xEE')
        on_packet_callback: Optional callback function called when packet detected
                           Signature: callback(timestamp_ns: int) -> None
                           Called in reader thread with packet timestamp
    """
    enable: bool = False
    start_marker: Optional[bytes] = None
    packet_length: Optional[int] = None
    end_marker: Optional[bytes] = None
    on_packet_callback: Optional[Callable[[int], None]] = None


@dataclass
class PacketInfo:
    """
    Information about a detected packet.
    
    packet_id: Sequential ID (resets per connection)
    start_timestamp: When first byte arrived (nanoseconds)
    stop_timestamp: When stop condition met (not used in listening mode)
    data: Packet payload
    stop_reason: "timeout", "terminator", "grace_timeout", "packet_complete"
    """
    packet_id: int
    start_timestamp: float
    stop_timestamp: float
    data: bytes
    stop_reason: str


@dataclass
class PortStatus:
    """
    Current port status.
    
    is_open: Port is opened
    is_reader_running: Reader thread is active
    buffer_capacity: Buffer size in bytes
    buffer_fill_bytes: Current buffer usage
    buffer_fill_percent: Fill percentage (0-100)
    buffer_overflows: Number of buffer overflows
    total_bytes_received: Total bytes since connection
    total_packets: Total packets detected
    """
    is_open: bool
    is_reader_running: bool
    buffer_capacity: int
    buffer_fill_bytes: int
    buffer_fill_percent: int
    buffer_overflows: int
    total_bytes_received: int
    total_packets: int


# =============================================================================
# UART Port Handle
# =============================================================================

@dataclass
class UARTPort:
    """
    UART port instance.
    
    Internal state - do not access directly.
    Use the public API functions.
    """
    config: UARTConfig
    
    # Serial port
    _serial_port: Optional[serial.Serial] = None
    _is_open: bool = False
    
    # Circular buffer
    _rx_buffer: Optional[CircularBufferHandle] = None
    
    # Reader thread
    _reader_thread: Optional[threading.Thread] = None
    _reader_running: bool = False
    _stop_reader_event: threading.Event = field(default_factory=threading.Event)
    
    # Statistics (protected by lock)
    _lock: threading.Lock = field(default_factory=threading.Lock)
    _total_bytes_received: int = 0
    _total_packets: int = 0
    _last_rx_timestamp: float = 0.0
    _listening_start_time: float = 0.0
    
    # Packet detection (for listening mode with packet detection)
    _packet_config: Optional[PacketConfig] = None
    _detected_packets: list = field(default_factory=list)
    _packet_buffer: bytearray = field(default_factory=bytearray)
    _packet_detection_active: bool = False
    _packet_start_timestamp: float = 0.0
    _packet_errors: int = 0  # Count of packets with end marker errors
    
    # Timestamp function
    _get_timestamp: Callable[[], float] = field(default=time.perf_counter)
    
    def __post_init__(self):
        """Initialize timestamp source."""
        if self.config.timestamp_source:
            self._get_timestamp = self.config.timestamp_source


# =============================================================================
# Port Lifecycle
# =============================================================================

def uart_create(config: UARTConfig) -> Tuple[Status, Optional[UARTPort]]:
    """
    Create UART port instance.
    
    Returns:
        (Status.OK, port) on success
        (Status.BAD_CONFIG, None) on invalid config
    """
    if not config.device or config.baudrate <= 0:
        return (Status.BAD_CONFIG, None)
    
    if not serial:
        return (Status.IO_ERROR, None)
    
    port = UARTPort(config=config)
    return (Status.OK, port)


def uart_open(port: UARTPort) -> Status:
    """
    Open serial port and initialize buffer.
    
    Returns:
        Status.OK on success
        Status.ALREADY_OPEN if already open
        Status.IO_ERROR on failure
    """
    if port._is_open:
        return Status.ALREADY_OPEN
    
    try:
        # Map parity
        parity_map = {
            'N': serial.PARITY_NONE,
            'E': serial.PARITY_EVEN,
            'O': serial.PARITY_ODD,
            'M': serial.PARITY_MARK,
            'S': serial.PARITY_SPACE
        }
        
        # Open serial port with FULL configuration
        port._serial_port = serial.Serial(
            port=port.config.device,
            baudrate=port.config.baudrate,
            bytesize=port.config.data_bits,
            parity=parity_map.get(port.config.parity, serial.PARITY_NONE),
            stopbits=port.config.stop_bits,
            timeout=0.01,  # Non-blocking with small timeout
            write_timeout=0.5,
            xonxoff=False,    # Disable software flow control
            rtscts=False,     # Disable hardware (RTS/CTS) flow control
            dsrdtr=False      # Disable hardware (DSR/DTR) flow control
        )
        
        # Create RX buffer
        status, buffer = cb_init(port.config.buffer_size)
        if status != BufferStatus.OK:
            port._serial_port.close()
            return Status.IO_ERROR
        
        port._rx_buffer = buffer
        port._is_open = True
        
        # Reset statistics
        with port._lock:
            port._total_bytes_received = 0
            port._total_packets = 0
            port._last_rx_timestamp = 0.0
        
        return Status.OK
        
    except Exception as e:
        return Status.IO_ERROR


def uart_close(port: UARTPort) -> Status:
    """
    Close serial port and cleanup resources.
    
    Stops reader thread if running.
    
    Returns:
        Status.OK on success
    """
    if not port._is_open:
        return Status.OK
    
    # Stop reader first
    if port._reader_running:
        uart_stop_reader(port)
    
    # Close serial port
    if port._serial_port:
        try:
            port._serial_port.close()
        except:
            pass
        port._serial_port = None
    
    # Cleanup buffer
    port._rx_buffer = None
    port._is_open = False
    
    return Status.OK


# =============================================================================
# Reader Thread
# =============================================================================

def uart_start_reader(port: UARTPort) -> Status:
    """
    Start background reader thread.
    
    Thread reads from serial port and writes to circular buffer.
    
    Returns:
        Status.OK on success
        Status.PORT_CLOSED if port not open
    """
    if not port._is_open:
        return Status.PORT_CLOSED
    
    if port._reader_running:
        return Status.OK  # Already running
    
    port._stop_reader_event.clear()
    port._reader_running = True
    
    port._reader_thread = threading.Thread(
        target=_reader_thread_func,
        args=(port,),
        daemon=True,
        name=f"UART-Reader-{port.config.device}"
    )
    port._reader_thread.start()
    
    return Status.OK


def uart_stop_reader(port: UARTPort) -> Status:
    """
    Stop background reader thread.
    
    Blocks until thread exits (with timeout).
    
    Returns:
        Status.OK on success
    """
    if not port._reader_running:
        return Status.OK
    
    port._stop_reader_event.set()
    
    if port._reader_thread:
        port._reader_thread.join(timeout=1.0)
        port._reader_thread = None
    
    port._reader_running = False
    
    return Status.OK


def _reader_thread_func(port: UARTPort) -> None:
    """
    Background reader thread implementation.
    
    Continuously reads from serial port and writes to circular buffer.
    Updates timestamps and byte counters.
    
    If packet detection is enabled, also detects packet boundaries
    and stores detected packets with timestamps.
    """
    while not port._stop_reader_event.is_set():
        try:
            if port._serial_port and port._serial_port.in_waiting > 0:
                # Read available data
                chunk = port._serial_port.read(port.config.read_chunk_size)
                
                if chunk:
                    # Write to circular buffer
                    cb_write(port._rx_buffer, chunk)
                    
                    # Update statistics
                    timestamp = port._get_timestamp()
                    with port._lock:
                        port._total_bytes_received += len(chunk)
                        port._last_rx_timestamp = timestamp
                    
                    # Packet detection (if enabled)
                    if port._packet_detection_active and port._packet_config:
                        _detect_packets_in_chunk(port, chunk, timestamp)
            
            else:
                # No data available, sleep briefly
                time.sleep(0.001)
        
        except Exception:
            # IO error - exit thread
            break


def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) -> None:
    """
    Detect packets in received chunk.

    Uses configured packet format (start marker, length, end marker).
    Stores complete packets in _detected_packets list with timestamps.

    Packet format: [START_MARKER][DATA][END_MARKER]
    Example: EF FE [14 bytes] EE

    Args:
        port: UART port instance
        chunk: Received data chunk
        timestamp: Timestamp when chunk received
    """
    if not port._packet_config or not port._packet_config.enable:
        return

    cfg = port._packet_config

    # Add chunk to packet buffer
    port._packet_buffer.extend(chunk)

    # Process buffer looking for complete packets
    while len(port._packet_buffer) >= (cfg.packet_length or 0):
        # Look for start marker
        if cfg.start_marker:
            # Find start marker position
            start_idx = port._packet_buffer.find(cfg.start_marker)

            if start_idx == -1:
                # No start marker found - keep searching in larger buffer
                # Only clear if buffer gets excessively large (prevent memory issues)
                max_buffer_size = 1024 * 1024  # 1MB should be plenty for packet detection
                if len(port._packet_buffer) > max_buffer_size:
                    # Keep last chunk that might contain start of next packet
                    keep_size = min(cfg.packet_length * 2, 1024)
                    port._packet_buffer = port._packet_buffer[-keep_size:]
                break
            
            # Remove everything before start marker
            if start_idx > 0:
                port._packet_buffer = port._packet_buffer[start_idx:]
            
            # Check if we have enough bytes for complete packet
            if len(port._packet_buffer) < cfg.packet_length:
                break  # Wait for more data
            
            # Extract potential packet
            packet_bytes = bytes(port._packet_buffer[:cfg.packet_length])
            
            # Verify end marker (if configured)
            if cfg.end_marker:
                expected_end_pos = cfg.packet_length - len(cfg.end_marker)
                actual_end = packet_bytes[expected_end_pos:]
                
                if actual_end != cfg.end_marker:
                    # Invalid packet - count error, discard first byte and try again
                    port._packet_errors += 1
                    port._packet_buffer.pop(0)
                    continue
            
            # Valid packet found!
            # Timestamp at packet START (when we found start marker)
            if port._packet_start_timestamp == 0.0:
                port._packet_start_timestamp = timestamp

            # Create packet info
            with port._lock:
                port._total_packets += 1
                packet_id = port._total_packets
            
            packet_info = PacketInfo(
                packet_id=packet_id,
                start_timestamp=int(port._packet_start_timestamp * 1e9),  # Convert to nanoseconds
                stop_timestamp=int(timestamp * 1e9),  # Not used, but filled anyway
                data=packet_bytes,
                stop_reason="packet_complete"
            )
            
            # Store packet
            port._detected_packets.append(packet_info)
            
            # Call callback if provided (with timestamp in nanoseconds)
            if cfg.on_packet_callback:
                try:
                    cfg.on_packet_callback(packet_info.start_timestamp)
                except Exception as e:
                    # Don't crash reader thread if callback fails
                    print(f"[ERROR] Packet callback exception: {e}")
                    import traceback
                    traceback.print_exc()
            
            # Remove packet from buffer
            port._packet_buffer = port._packet_buffer[cfg.packet_length:]
            
            # Reset start timestamp for next packet
            port._packet_start_timestamp = 0.0
        
        else:
            # No start marker - just use fixed length
            if cfg.packet_length and len(port._packet_buffer) >= cfg.packet_length:
                packet_bytes = bytes(port._packet_buffer[:cfg.packet_length])
                
                with port._lock:
                    port._total_packets += 1
                    packet_id = port._total_packets
                
                packet_info = PacketInfo(
                    packet_id=packet_id,
                    start_timestamp=int(timestamp * 1e9),
                    stop_timestamp=int(timestamp * 1e9),
                    data=packet_bytes,
                    stop_reason="packet_complete"
                )
                
                port._detected_packets.append(packet_info)
                
                # Call callback if provided (with timestamp in nanoseconds)
                if cfg.on_packet_callback:
                    try:
                        cfg.on_packet_callback(packet_info.start_timestamp)
                    except Exception as e:
                        # Don't crash reader thread if callback fails
                        print(f"[ERROR] Packet callback exception: {e}")
                        import traceback
                        traceback.print_exc()
                
                port._packet_buffer = port._packet_buffer[cfg.packet_length:]
            else:
                break


# =============================================================================
# Write Operations
# =============================================================================

def uart_write(port: UARTPort, data: bytes) -> Tuple[Status, int]:
    """
    Write data to serial port.
    
    Returns:
        (Status.OK, bytes_written) on success
        (Status.PORT_CLOSED, 0) if port not open
        (Status.IO_ERROR, 0) on write failure
    """
    if not port._is_open or not port._serial_port:
        return (Status.PORT_CLOSED, 0)
    
    try:
        written = port._serial_port.write(data)
        port._serial_port.flush()
        return (Status.OK, written)
    
    except Exception:
        return (Status.IO_ERROR, 0)


# =============================================================================
# Packet Detection (Request-Response Mode)
# =============================================================================

def uart_send_and_receive(port: UARTPort, tx_data: bytes, 
                          timeout_ms: Optional[int] = None) -> Tuple[Status, Optional[PacketInfo]]:
    """
    Send data and wait for response packet (request-response mode).
    
    Uses configured stop condition (timeout or terminator).
    Timeout starts immediately after send.
    
    Args:
        port: UART port instance
        tx_data: Data to transmit
        timeout_ms: Override configured timeout (optional)
    
    Returns:
        (Status.OK, PacketInfo) on success
        (Status.TIMEOUT, None) on timeout
        (Status.PORT_CLOSED, None) if port not ready
    """
    if not port._is_open or not port._reader_running:
        return (Status.PORT_CLOSED, None)
    
    # Use configured timeout or override
    timeout = timeout_ms if timeout_ms is not None else port.config.stop_timeout_ms

    # CRITICAL: Flush serial input buffer BEFORE sending to clear any background traffic
    # This ensures we only capture the response to OUR command, not stale data
    try:
        if port._serial_port:
            port._serial_port.reset_input_buffer()
    except Exception:
        pass  # Some platforms don't support this - continue anyway

    # Snapshot buffer position AFTER flush
    start_w = cb_w_abs(port._rx_buffer)

    # Send data
    status, _ = uart_write(port, tx_data)
    if status != Status.OK:
        return (status, None)
    
    # Start timestamp
    start_time = port._get_timestamp()
    timeout_s = timeout / 1000.0
    
    # Wait for response
    return _wait_for_packet(
        port=port,
        start_w=start_w,
        start_time=start_time,
        timeout_s=timeout_s,
        grace_period=False
    )


def uart_send_and_read_pgkomm2(port: UARTPort,
                               tx_data: bytes,
                               capture_max_ms: int = 30,
                               max_frames: int = 10,
                               log_callback = None) -> Tuple[Status, Optional[list]]:
    """
    Send PGKomm2 command and read response frames.

    Uses EXACT logic from old working code (uart_old/pgkomm.py):
    - Single timeout window for entire operation
    - Read immediately when bytes arrive
    - Stop immediately when HP response detected
    - No sleep, no delays - pure spinning
    - BCC validation rejects corrupted frames

    PGKomm2 protocol (V-ZUG spec A5.5093D-AB):
    - Frame format: DD 22 | ADR1 ADR2 | LEN | DATA(0-255) | BCC
    - Response time: < 15 ms (spec, ideal conditions)
    - Multiple frames may be returned (SB status + PH echo + HP response)
    - Length-delimited, no terminator bytes
    - Background telemetry: Device sends unsolicited SB frames continuously

    Args:
        port: UART port instance
        tx_data: PGKomm2 command to transmit (must start with DD 22)
        capture_max_ms: Total capture window in ms (default 30ms, spec says <15ms but real-world needs margin)
        max_frames: Maximum number of frames to parse (safety limit)
        log_callback: Optional callback(direction, message) for logging (e.g., to GUI)

    Returns:
        (Status.OK, [frame1, frame2, ...]) on success
        (Status.TIMEOUT, None) if no response within timeout
        (Status.PORT_CLOSED, None) if port not ready
        (Status.IO_ERROR, None) on read/write error

    Example:
        # Send PGKomm2 command
        status, frames = uart_send_and_read_pgkomm2(
            port,
            bytes.fromhex("DD 22 50 48 02 43 4F 16"),
            capture_max_ms=30  # Default, can be adjusted if needed
        )
        if status == Status.OK:
            for frame in frames:
                print(f"Frame: {frame.hex(' ')}")

        # BCC errors are logged to console automatically
    """
    if not port._is_open or not port._serial_port:
        return (Status.PORT_CLOSED, None)

    # Save original timeout
    original_timeout = port._serial_port.timeout

    # Temporarily stop reader thread to get exclusive serial access
    reader_was_running = port._reader_running
    if reader_was_running:
        uart_stop_reader(port)

    try:
        # Flush serial input buffer (clear background traffic)
        try:
            port._serial_port.reset_input_buffer()
        except Exception:
            pass

        # Set non-blocking timeout
        port._serial_port.timeout = 0

        # Send command
        status, _ = uart_write(port, tx_data)
        if status != Status.OK:
            return (status, None)

        # Calculate deadline (single timeout for entire operation)
        start_time = time.time()
        deadline = start_time + (capture_max_ms / 1000.0)

        rx_buffer = bytearray()
        collected_frames = []
        echo_frame = None
        reply_frame = None

        # Single loop - read directly from serial until HP or timeout (like old code)
        while time.time() < deadline:
            # Read available data IMMEDIATELY from serial port
            n = port._serial_port.in_waiting or 0
            if n:
                chunk = port._serial_port.read(n)
                if chunk:
                    rx_buffer += chunk

                    # Try to extract complete frames as they arrive
                    while True:
                        frame = _extract_pgkomm2_frame(rx_buffer, log_callback)
                        if frame is None:
                            break  # Need more bytes

                        # Collect frame
                        collected_frames.append(frame)

                        # Check frame type
                        if len(frame) >= 5:
                            adr1, adr2 = frame[2], frame[3]

                            # First complete frame is typically PH echo
                            if echo_frame is None and adr1 == 0x50 and adr2 == 0x48:  # PH
                                echo_frame = frame
                                continue

                            # Prefer HP and stop looking once we have it
                            if adr1 == 0x48 and adr2 == 0x50:  # HP
                                reply_frame = frame
                                break

                            # If it's neither PH nor HP and we have no echo yet
                            if echo_frame is None:
                                echo_frame = frame

                    # Stop immediately if we have HP
                    if reply_frame is not None:
                        break

            # No sleep - pure spinning (old code does this)

        # Return results - ONLY success if we got HP response!
        if reply_frame is not None:
            # Got HP response - success!
            return (Status.OK, collected_frames)
        elif len(collected_frames) > 0:
            # Got frames but no HP response (only SB broadcasts or PH echo without answer)
            msg = f"[PGKOMM2] TIMEOUT: Got {len(collected_frames)} frame(s) but no HP response"
            if log_callback:
                log_callback("ERROR", msg)
            else:
                print(msg)
            return (Status.TIMEOUT, None)
        elif len(rx_buffer) > 0:
            # Unparseable data - log for debugging
            msg = f"[PGKOMM2] IO_ERROR: Unparsed buffer ({len(rx_buffer)} bytes): {rx_buffer.hex(' ').upper()}"
            if log_callback:
                log_callback("ERROR", msg)
            else:
                print(msg)
            return (Status.IO_ERROR, None)
        else:
            # No response
            msg = f"[PGKOMM2] TIMEOUT: No data received within {capture_max_ms}ms"
            if log_callback:
                log_callback("ERROR", msg)
            else:
                print(msg)
            return (Status.TIMEOUT, None)

    except Exception as e:
        return (Status.IO_ERROR, None)

    finally:
        # Restore timeout
        try:
            if port._serial_port:
                port._serial_port.timeout = original_timeout
        except Exception:
            pass

        # Restart reader thread if it was running
        if reader_was_running:
            uart_start_reader(port)


def _extract_pgkomm2_frame(buffer: bytearray, log_callback=None) -> Optional[bytes]:
    """
    Extract ONE complete PGKomm2 frame from buffer (destructive).

    Uses the EXACT logic from the old working code (uart_old/pgkomm.py).

    Searches for DD 22 header, reads LEN, extracts complete frame,
    validates BCC checksum, and REMOVES it from the buffer.

    Frame format: [DD][22][ADR1][ADR2][LEN][DATA...][BCC]
    BCC = XOR of all bytes from ADR1 through last DATA byte

    Args:
        buffer: Bytearray to extract from (will be modified!)
        log_callback: Optional callback(direction, message) for logging

    Returns:
        Complete frame as bytes, or None if no complete frame available
        Corrupted frames (BCC mismatch) are rejected and return None
    """
    MAGIC = 0xDD
    INVMAGIC = 0x22

    # Hunt for header DD 22
    i = 0
    blen = len(buffer)

    while i + 1 < blen:
        if buffer[i] == MAGIC and buffer[i + 1] == INVMAGIC:
            # Have header; check if we have at least up to LEN
            if i + 5 > blen:
                # Need more bytes for ADR1/ADR2/LEN
                return None

            adr1 = buffer[i + 2]
            adr2 = buffer[i + 3]
            length = buffer[i + 4]
            total = 6 + length  # full frame size

            if i + total <= blen:
                # We have the whole frame
                frame = bytes(buffer[i:i + total])

                # Validate BCC (XOR from ADR1 through DATA)
                # BCC = frame[-1], should equal XOR of frame[2:-1]
                calculated_bcc = 0
                for byte in frame[2:-1]:  # ADR1, ADR2, LEN, DATA...
                    calculated_bcc ^= byte

                received_bcc = frame[-1]

                if calculated_bcc != received_bcc:
                    # BCC mismatch - frame corrupted!
                    adr_str = f"{frame[2]:02X} {frame[3]:02X}" if len(frame) >= 4 else "??"
                    error_msg = f"[PGKOMM2] ✗ BCC FAIL: ADR={adr_str}, calc={calculated_bcc:02X}, recv={received_bcc:02X}"
                    frame_msg = f"[PGKOMM2]   Frame: {frame.hex(' ').upper()}"

                    # Log via callback or print
                    if log_callback:
                        log_callback("ERROR", error_msg)
                        log_callback("ERROR", frame_msg)
                    else:
                        print(error_msg)
                        print(frame_msg)

                    # Drop this frame and continue searching
                    del buffer[:i + total]
                    return None  # Reject corrupted frame

                # BCC valid - frame is good!
                del buffer[:i + total]
                return frame
            else:
                # Header found but incomplete body — wait for more bytes
                return None
        else:
            i += 1

    # If we advanced i without finding a header, drop garbage to i to avoid re-scanning
    # BUT: Only if we either reached the end OR the buffer doesn't start with MAGIC
    if i > 0 and (i >= blen or not (blen >= 2 and buffer[0] == MAGIC and buffer[1] == INVMAGIC)):
        del buffer[:i]

    return None


def _parse_pgkomm2_frames(buffer: bytearray, max_frames: int = 10) -> list:
    """
    Parse PGKomm2 frames from buffer.

    Frame format: DD 22 | ADR1 ADR2 | LEN | DATA | BCC
    Total size: 6 + LEN bytes

    Args:
        buffer: Raw data buffer
        max_frames: Maximum frames to extract (safety limit)

    Returns:
        List of frame bytes (each frame as bytes object)
    """
    MAGIC = 0xDD
    INVMAGIC = 0x22
    MIN_FRAME_SIZE = 6  # DD 22 ADR1 ADR2 LEN BCC (with LEN=0)

    frames = []
    pos = 0

    while pos < len(buffer) and len(frames) < max_frames:
        # Look for frame header DD 22
        if pos + 1 >= len(buffer):
            break

        if buffer[pos] == MAGIC and buffer[pos + 1] == INVMAGIC:
            # Found potential frame start
            if pos + 5 >= len(buffer):  # Need at least ADR1 ADR2 LEN
                break

            # Extract length byte
            length = buffer[pos + 4]
            frame_size = 6 + length  # DD 22 ADR1 ADR2 LEN DATA(length) BCC

            # Check if we have complete frame
            if pos + frame_size <= len(buffer):
                # Extract complete frame
                frame = bytes(buffer[pos:pos + frame_size])
                frames.append(frame)
                pos += frame_size
            else:
                # Incomplete frame at end of buffer
                break
        else:
            # Not a frame start, advance
            pos += 1

    return frames


# =============================================================================
# Listening Mode (Continuous, No Auto-Stop)
# =============================================================================

def uart_start_listening(port: UARTPort) -> Status:
    """
    Start listening mode - continuous data collection with no stop condition.
    
    Data fills buffer continuously. Use uart_read_buffer() to get current contents.
    Call uart_stop_listening() to stop.
    
    Returns:
        Status.OK on success
        Status.PORT_CLOSED if port not ready
    """
    if not port._is_open or not port._reader_running:
        return Status.PORT_CLOSED
    
    # Listening mode has no special state - just reader running
    # Mark listening start time for potential later use
    with port._lock:
        port._listening_start_time = port._get_timestamp()
    
    return Status.OK


def uart_stop_listening(port: UARTPort) -> Status:
    """
    Stop listening mode.
    
    Returns:
        Status.OK
    """
    # Nothing special to do - reader keeps running
    # Just mark end time
    with port._lock:
        port._listening_start_time = 0.0
    
    # Stop packet detection
    port._packet_detection_active = False
    
    return Status.OK


def uart_start_listening_with_packets(port: UARTPort, packet_config: PacketConfig) -> Status:
    """
    Start listening mode WITH packet detection.
    
    Reader thread will:
    - Fill circular buffer (continuous logging)
    - Detect packet boundaries based on packet_config
    - Store each detected packet with timestamp and count
    - Call optional callback when packet detected (real-time trigger)
    
    Args:
        port: UART port instance
        packet_config: Packet detection configuration
    
    Returns:
        Status.OK on success
        Status.PORT_CLOSED if port not ready
    
    Example without callback:
        # For packet format: EF FE [14 bytes] EE (17 bytes total)
        packet_config = PacketConfig(
            enable=True,
            start_marker=b'\\xEF\\xFE',
            packet_length=17,
            end_marker=b'\\xEE'
        )
        uart_start_listening_with_packets(port, packet_config)
    
    Example with callback (real-time trigger):
        def on_packet_detected(timestamp_ns: int):
            '''Called immediately when packet detected.'''
            # Trigger I2C read or other action
            i2c_read_angle(i2c_port)
        
        packet_config = PacketConfig(
            enable=True,
            start_marker=b'\\xEF\\xFE',
            packet_length=17,
            end_marker=b'\\xEE',
            on_packet_callback=on_packet_detected
        )
        uart_start_listening_with_packets(port, packet_config)
    """
    if not port._is_open or not port._reader_running:
        return Status.PORT_CLOSED
    
    # Configure packet detection
    port._packet_config = packet_config
    port._packet_buffer.clear()
    port._packet_start_timestamp = 0.0
    port._packet_errors = 0  # Reset error counter

    # Mark listening start time
    with port._lock:
        port._listening_start_time = port._get_timestamp()
    
    # Enable packet detection in reader thread
    port._packet_detection_active = packet_config.enable
    
    return Status.OK


def uart_get_detected_packets(port: UARTPort) -> list:
    """
    Get all packets detected since listening started.
    
    Returns list of PacketInfo objects, each containing:
    - packet_id: Sequential packet number (1, 2, 3, ...)
    - start_timestamp: When packet started (nanoseconds since epoch)
    - data: Raw packet bytes
    
    Returns:
        List of PacketInfo objects
    
    Example:
        packets = uart_get_detected_packets(port)
        print(f"Detected {len(packets)} packets")
        for pkt in packets:
            print(f"Packet {pkt.packet_id} at {pkt.start_timestamp}ns")
    """
    return port._detected_packets.copy()


def uart_get_packet_errors(port: UARTPort) -> int:
    """
    Get number of packet errors (end marker mismatches).

    Returns:
        Number of packets with end marker errors since listening started
    """
    return port._packet_errors


def uart_clear_detected_packets(port: UARTPort) -> Status:
    """
    Clear detected packets list.
    
    Call at start of each RUN to reset packet counter and list.
    
    Returns:
        Status.OK
    """
    port._detected_packets.clear()
    port._packet_buffer.clear()
    port._packet_start_timestamp = 0.0
    port._packet_errors = 0  # Reset error counter

    with port._lock:
        port._total_packets = 0

    return Status.OK


def uart_read_buffer(port: UARTPort, max_bytes: int = 0) -> Tuple[Status, bytes]:
    """
    Read current buffer contents (for listening mode).
    
    Args:
        port: UART port
        max_bytes: Max bytes to read (0 = all available, or last N bytes if buffer larger)
    
    Returns:
        (Status.OK, data) on success
        (Status.PORT_CLOSED, b"") if port not ready
    """
    if not port._is_open or not port._rx_buffer:
        return (Status.PORT_CLOSED, b"")
    
    current_w = cb_w_abs(port._rx_buffer)
    
    if current_w == 0:
        return (Status.OK, b"")
    
    if max_bytes > 0:
        # Read last N bytes
        start_w = max(0, current_w - max_bytes)
    else:
        # Read all available (up to buffer capacity)
        capacity = cb_capacity(port._rx_buffer)
        start_w = max(0, current_w - capacity)
    
    status, data = cb_copy_span(port._rx_buffer, start_w, current_w)
    
    if status != BufferStatus.OK:
        return (Status.IO_ERROR, b"")
    
    return (Status.OK, data)


# =============================================================================
# Packet Detection (Polling Mode)
# =============================================================================

def uart_poll_packet(port: UARTPort) -> Tuple[Status, Optional[PacketInfo]]:
    """
    Poll for next packet (polling mode).
    
    Grace period: Waits for first byte, then timeout starts.
    
    Flow:
        1. Wait up to grace_timeout_ms for first byte
        2. Once first byte arrives, start stop_timeout_ms
        3. If grace expires without byte, start timeout anyway
        4. Detect stop condition (timeout or terminator)
    
    Returns:
        (Status.OK, PacketInfo) on success
        (Status.TIMEOUT_NO_DATA, None) if no data after grace + timeout
        (Status.PORT_CLOSED, None) if port not ready
    """
    if not port._is_open or not port._reader_running:
        return (Status.PORT_CLOSED, None)
    
    if not port.config.polling_mode:
        # Not configured for polling - use regular timeout
        start_w = cb_w_abs(port._rx_buffer)
        start_time = port._get_timestamp()
        timeout_s = port.config.stop_timeout_ms / 1000.0
        
        return _wait_for_packet(
            port=port,
            start_w=start_w,
            start_time=start_time,
            timeout_s=timeout_s,
            grace_period=False
        )
    
    # Polling mode with grace period
    start_w = cb_w_abs(port._rx_buffer)
    start_time = port._get_timestamp()
    grace_s = port.config.grace_timeout_ms / 1000.0
    timeout_s = port.config.stop_timeout_ms / 1000.0
    
    # Phase 1: Grace period - wait for first byte
    first_byte_seen = False
    grace_start = start_time
    
    while (port._get_timestamp() - grace_start) < grace_s:
        current_w = cb_w_abs(port._rx_buffer)
        if current_w > start_w:
            # First byte arrived!
            first_byte_seen = True
            break
        time.sleep(0.001)
    
    # Phase 2: Timeout starts (whether or not byte arrived)
    return _wait_for_packet(
        port=port,
        start_w=start_w,
        start_time=port._get_timestamp(),  # Reset start time
        timeout_s=timeout_s,
        grace_period=False,
        grace_expired_no_data=(not first_byte_seen)
    )


def _wait_for_packet(port: UARTPort, start_w: int, start_time: float,
                     timeout_s: float, grace_period: bool,
                     grace_expired_no_data: bool = False) -> Tuple[Status, Optional[PacketInfo]]:
    """
    Internal: Wait for stop condition and collect packet.

    Args:
        port: UART port
        start_w: Buffer write position at start
        start_time: Start timestamp
        timeout_s: Timeout in seconds
        grace_period: If True, timeout starts after first byte
        grace_expired_no_data: Grace expired without any byte

    Returns:
        (Status, PacketInfo or None)
    """
    mode = port.config.stop_mode
    first_byte_seen = False
    first_byte_time = 0.0
    last_rx_time = start_time

    while True:
        now = port._get_timestamp()

        # Check if port was closed during execution
        if not port._is_open or port._rx_buffer is None:
            return (Status.PORT_CLOSED, None)

        current_w = cb_w_abs(port._rx_buffer)
        
        # Check for new data
        if current_w > start_w:
            if not first_byte_seen:
                first_byte_seen = True
                first_byte_time = now
            
            # Update last RX time from reader thread
            with port._lock:
                last_rx_time = port._last_rx_timestamp
        
        # Grace period: wait for first byte before starting timeout
        if grace_period and not first_byte_seen:
            if (now - start_time) >= timeout_s:
                # Grace expired, no data
                if grace_expired_no_data:
                    return (Status.TIMEOUT_NO_DATA, None)
                # Start regular timeout now
                grace_period = False
                start_time = now
            time.sleep(0.001)
            continue
        
        # Timeout mode: silence timeout
        if mode == StopConditionMode.TIMEOUT:
            if first_byte_seen:
                silence = now - last_rx_time
                if silence >= (timeout_s):
                    # Stop condition: timeout met
                    status, data = cb_copy_span(port._rx_buffer, start_w, current_w)
                    if status != BufferStatus.OK:
                        return (Status.IO_ERROR, None)

                    return _create_packet_info(
                        port=port,
                        data=data,
                        start_time=first_byte_time,
                        stop_time=now,
                        stop_reason="timeout"
                    )
            else:
                # No byte yet, check absolute timeout
                if (now - start_time) >= timeout_s:
                    return (Status.TIMEOUT_NO_DATA, None)
        
        # Terminator mode: look for specific byte
        elif mode == StopConditionMode.TERMINATOR:
            if current_w > start_w:
                # Read current data
                status, data = cb_copy_span(port._rx_buffer, start_w, current_w)
                if status != BufferStatus.OK:
                    return (Status.IO_ERROR, None)
                
                # Check for terminator
                if port.config.stop_terminator in data:
                    return _create_packet_info(
                        port=port,
                        data=data,
                        start_time=first_byte_time if first_byte_seen else start_time,
                        stop_time=now,
                        stop_reason="terminator"
                    )
            
            # Absolute timeout fallback
            if (now - start_time) >= timeout_s:
                return (Status.TIMEOUT, None)
        
        time.sleep(0.001)


def _create_packet_info(port: UARTPort, data: bytes, start_time: float,
                       stop_time: float, stop_reason: str) -> Tuple[Status, PacketInfo]:
    """Create packet info and increment counter."""
    with port._lock:
        port._total_packets += 1
        packet_id = port._total_packets
    
    packet = PacketInfo(
        packet_id=packet_id,
        start_timestamp=start_time,
        stop_timestamp=stop_time,
        data=data,
        stop_reason=stop_reason
    )
    
    return (Status.OK, packet)


# =============================================================================
# Status Query
# =============================================================================

def uart_get_status(port: UARTPort) -> PortStatus:
    """
    Get current port status.
    
    Returns:
        PortStatus with current metrics
    """
    if not port._rx_buffer:
        return PortStatus(
            is_open=port._is_open,
            is_reader_running=port._reader_running,
            buffer_capacity=0,
            buffer_fill_bytes=0,
            buffer_fill_percent=0,
            buffer_overflows=0,
            total_bytes_received=0,
            total_packets=0
        )
    
    with port._lock:
        total_bytes = port._total_bytes_received
        total_packets = port._total_packets
    
    return PortStatus(
        is_open=port._is_open,
        is_reader_running=port._reader_running,
        buffer_capacity=cb_capacity(port._rx_buffer),
        buffer_fill_bytes=cb_fill_bytes(port._rx_buffer),
        buffer_fill_percent=cb_fill_pct(port._rx_buffer),
        buffer_overflows=cb_overflows(port._rx_buffer),
        total_bytes_received=total_bytes,
        total_packets=total_packets
    )
