# -----------------------
# File: components/uart/pgkomm2.py
#
# PGKomm2 framing + send logic (NON-BLOCKING)
# -------------------------------------------
# Frame format:
#   [0xDD][0x22][ADR1][ADR2][LEN][DATA...][BCC]
#   Total length = 6 + LEN (LEN at index 4)
#
# Goals:
# - Zero blocking: no sleep, no flush, no read timeouts.
# - Parse using a rolling buffer fed by ser.in_waiting.
# - Stop as soon as a COMPLETE frame is available.
# - Prefer HP; fall back to the last complete frame (often echo PH).
# - One concise log line per exchange: "→ TX  ← RX"
# -----------------------

from __future__ import annotations
from components.scheduler.timebase import now_ns
from components.scheduler.coordinator import claim_uart_capture, release_uart_capture
import config.config as config

MAGIC = 0xDD
INVMAGIC = 0x22
ADR_PH = (0x50, 0x48)  # 'P','H' (typical TX echo)
ADR_HP = (0x48, 0x50)  # 'H','P' (slave reply we want)

# ---------------------------
# Utils
# ---------------------------


def fmt_hex_ascii(data: bytes) -> str:
    """HEX + ASCII, but hide leading DD 22 if present (constant PGKomm2 header)."""
    hex_part = data.hex(" ").upper()
    ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in data)
    return f"{hex_part}    |    '{ascii_part}'"


def view_hex_no_magic(data: bytes) -> str:
    """HEX view, but skip the leading DD 22 if present."""
    return data.hex(" ").upper()


def view_ascii_no_magic(data: bytes) -> str:
    """ASCII view, but skip the leading DD 22 if present."""
    if len(data) >= 2 and data[0] == MAGIC and data[1] == INVMAGIC:
        data = data[2:]
    return "".join(chr(b) if 32 <= b <= 126 else "." for b in data)


def _is_ph_echo(frame: bytes) -> bool:
    return len(frame) >= 6 and frame[2] == ADR_PH[0] and frame[3] == ADR_PH[1]


def _is_hp_reply(frame: bytes) -> bool:
    return len(frame) >= 6 and frame[2] == ADR_HP[0] and frame[3] == ADR_HP[1]


# ---------------------------
# Non-blocking frame parser
# ---------------------------


def _extract_one_frame_from(buf: bytearray) -> bytes | None:
    """
    Non-blocking parser: try to cut ONE complete PGKomm2 frame out of 'buf'.
    If a full frame is found, it is REMOVED from 'buf' and returned (bytes).
    If not enough bytes yet, returns None and leaves 'buf' as-is.
    """
    # Hunt for header DD 22
    i = 0
    blen = len(buf)
    while i + 1 < blen:
        if buf[i] == MAGIC and buf[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
                break
            adr1 = buf[i + 2]
            adr2 = buf[i + 3]
            length = buf[i + 4]
            total = 6 + length  # full frame size

            if i + total <= blen:
                # We have the whole frame
                frame = bytes(buf[i : i + total])
                # Drop consumed bytes from buffer
                del buf[: i + total]
                return frame
            else:
                # Header found but incomplete body — wait for more bytes
                break
        else:
            i += 1

    # If we advanced i without finding a header, drop garbage to i to avoid re-scanning
    if i > 0 and (
        i >= blen or not (blen >= 2 and buf[0] == MAGIC and buf[1] == INVMAGIC)
    ):
        del buf[:i]
    return None


# ---------------------------
# Public API (non-blocking)
# ---------------------------


# --- replace your current send_pgkomm2 with this version ---


def send_pgkomm2(ser, hex_command: str, log, capture_max_ms: int = 15):
    """
    Non-blocking PGKomm2 TX/RX with the exact log style requested:

      [..] → <TX_HEX>    |    '<TX_ASCII>'
      [..] ← <ECHO_HEX> - <REPLY_HEX>    |    '<REPLY_ASCII>'
      or, if no reply:
      [..] ← <ECHO_HEX>    |    '<ECHO_ASCII>'

    - Uses a rolling buffer + LEN-based framing (no blocking).
    - Prefers HP; falls back to echo if no reply before the guard ends.
    """
    # Basic checks & parse TX
    if not config.DEBUG_MODE:
        if not ser or not ser.is_open:
            log("error", "⚠️ Not connected.")
            return
    try:
        tx = bytes.fromhex(hex_command.strip())
    except ValueError:
        log("error", "❌ Invalid hex string format.")
        return

    if not (
        len(tx) >= 6 and tx[0] == MAGIC and tx[1] == INVMAGIC and len(tx) == 6 + tx[4]
    ):
        log("warning", "ℹ️ TX length pattern mismatch (expected 6+LEN). Sending anyway.")

    # Exclusive capture window
    deadline_ns = now_ns() + int(capture_max_ms) * 1_000_000
    if not claim_uart_capture(deadline_ns):
        log("warning", "⛔ Busy: could not acquire capture window.")
        return

    old_timeout = None
    try:
        # TX (non-blocking; no flush, no sleep)
        if not config.DEBUG_MODE:
            try:
                ser.reset_input_buffer()
            except Exception:
                pass
            ser.write(tx)

        # Line 1: TX view (hide magic in both hex + ascii)
        tx_hex = view_hex_no_magic(tx)
        tx_ascii = view_ascii_no_magic(tx)
        log("success", f"→ {tx_hex}    |    '{tx_ascii}'")

        # DEBUG path: fabricate echo + reply
        if config.DEBUG_MODE:
            echo = tx
            reply = bytes.fromhex("DD 22 48 50 04 70 67 33 31 09")
            # Line 2 (with reply): ECHO_HEX - REPLY_HEX | 'REPLY_ASCII'
            line_left = view_hex_no_magic(echo)
            line_right = view_hex_no_magic(reply)
            right_ascii = view_ascii_no_magic(reply)
            log("info", f"← {line_left} - {line_right}    |    '{right_ascii}'")
            return reply

        # Non-blocking receive
        old_timeout = ser.timeout
        ser.timeout = 0  # non-blocking read()

        rx_buf = bytearray()
        echo_frame: bytes | None = None
        reply_frame: bytes | None = None

        while now_ns() < deadline_ns:
            n = ser.in_waiting or 0
            if n:
                rx_buf += ser.read(n)

                # Try to cut out complete frames as they arrive
                while True:
                    frame = _extract_one_frame_from(rx_buf)
                    if frame is None:
                        break

                    # First complete frame we see is typically PH echo
                    if echo_frame is None and _is_ph_echo(frame):
                        echo_frame = frame
                        continue

                    # Prefer HP and stop looking once we have it
                    if _is_hp_reply(frame):
                        reply_frame = frame
                        break

                    # If it's neither PH nor HP and we have no echo yet, use it as "echo-like"
                    if echo_frame is None:
                        echo_frame = frame

                if reply_frame is not None:
                    break  # we have the answer; stop immediately

            # If nothing waiting, just spin (no sleeps)

        # Line 2: compose exactly like your examples
        if reply_frame is not None:
            # With answer: show echo hex (if any) then reply hex, ASCII only for reply
            left_hex = view_hex_no_magic(echo_frame) if echo_frame else ""
            right_hex = view_hex_no_magic(reply_frame)
            right_ascii = view_ascii_no_magic(reply_frame)
            if left_hex:
                log("info", f"← {left_hex} - {right_hex}    |    '{right_ascii}'")
            else:
                log("info", f"← {right_hex}    |    '{right_ascii}'")
            return reply_frame

        if echo_frame is not None:
            # Only echo: show echo hex and its ASCII
            left_hex = view_hex_no_magic(echo_frame)
            left_ascii = view_ascii_no_magic(echo_frame)
            log("info", f"← {left_hex}    |    '{left_ascii}'")
            return echo_frame

        # Nothing at all
        log("info", "← <no response>")
        return

    except Exception as e:
        log("error", f"❌ UART send (PGKomm2) error: {e}")
    finally:
        try:
            if old_timeout is not None and ser:
                ser.timeout = old_timeout
        except Exception:
            pass
        release_uart_capture()
