"""Bridge diagnostics — HS window timing, post-inject capture, SCR path latency."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from pog422_fixture_core import _hex_prefix

if TYPE_CHECKING:
    from pog422_fixture_core import EventLog

POST_INJECT_MAX_US = 50_000.0
SCR_PRIORITY_MSGS = ("0B", "03", "02")
MACHINE_BRIDGE_POS = 4
TRUNK_BRIDGE_POS = 3


def hs_window_us(window_start_perf_ns: int | None, inactive_perf_ns: int) -> float | None:
    if window_start_perf_ns is None:
        return None
    return round((inactive_perf_ns - window_start_perf_ns) / 1000.0, 1)


@dataclass
class _PostInjectWatch:
    inject_end_perf_ns: int
    frame: bytes
    msg: str
    tid: str | None


class BridgeDiagnostics:
    """Correlate inject, HS windows, and SCR bridge-path timing on pos 3 / 4."""

    def __init__(self, fixture_pos: int, event_log: EventLog) -> None:
        self.fixture_pos = fixture_pos
        self.event_log = event_log
        self._post_inject: _PostInjectWatch | None = None
        self._pos4_native_03_perf_ns: int | None = None
        self._pos4_native_03_tid: str | None = None
        self._pos4_eth_rx_0b_perf_ns: int | None = None
        self._pos4_eth_rx_0b_tid: str | None = None
        self._pos3_eth_rx_03_perf_ns: int | None = None
        self._pos3_eth_rx_03_tid: str | None = None
        self._pos3_eth_rx_0b_perf_ns: int | None = None
        self._pos3_eth_rx_0b_tid: str | None = None
        self._last_scr_trunk_inject_perf_ns: int | None = None
        self._last_scr_trunk_inject_tid: str | None = None

    def tick(self, now_perf_ns: int | None = None) -> None:
        now = now_perf_ns if now_perf_ns is not None else self._now()
        self._expire_post_inject(now)

    def arm_post_inject(self, frame: bytes, msg: str, tid: str | None, inject_end_perf_ns: int) -> None:
        self._post_inject = _PostInjectWatch(
            inject_end_perf_ns=inject_end_perf_ns,
            frame=frame,
            msg=msg,
            tid=tid,
        )

    def on_hs_inactive_fields(
        self,
        *,
        window_start_perf_ns: int | None,
        inactive_perf_ns: int,
    ) -> dict[str, Any]:
        fields: dict[str, Any] = {}
        ws = hs_window_us(window_start_perf_ns, inactive_perf_ns)
        if ws is not None:
            fields["hs_window_us"] = ws
        return fields

    def on_eth_rx(self, msg: str, tid: str | None, perf_ns: int | None = None) -> None:
        now = perf_ns if perf_ns is not None else self._now()
        if self.fixture_pos == MACHINE_BRIDGE_POS and msg == "0B":
            self._pos4_eth_rx_0b_perf_ns = now
            self._pos4_eth_rx_0b_tid = tid
        if self.fixture_pos == TRUNK_BRIDGE_POS and msg == "03":
            self._pos3_eth_rx_03_perf_ns = now
            self._pos3_eth_rx_03_tid = tid
        if self.fixture_pos == TRUNK_BRIDGE_POS and msg == "0B":
            self._pos3_eth_rx_0b_perf_ns = now
            self._pos3_eth_rx_0b_tid = tid
            self.event_log.emit("scr_trunk_0b_eth", msg=msg, tid=tid)

    def on_eth_to_uart_done(
        self,
        frame: bytes,
        msg: str,
        tid: str | None,
        queue_wait_us: float,
        perf_ns: int | None = None,
    ) -> None:
        now = perf_ns if perf_ns is not None else self._now()
        self.arm_post_inject(frame, msg, tid, now)
        if self.fixture_pos == TRUNK_BRIDGE_POS and msg == "03":
            trunk_eth_us = None
            if self._pos3_eth_rx_03_perf_ns is not None:
                trunk_eth_us = round((now - self._pos3_eth_rx_03_perf_ns) / 1000.0, 1)
            self.event_log.emit(
                "scr_trunk_03_inject",
                msg=msg,
                tid=tid,
                queue_wait_us=round(queue_wait_us, 1),
                trunk_eth_us=trunk_eth_us,
            )
            self._last_scr_trunk_inject_perf_ns = now
            self._last_scr_trunk_inject_tid = tid
            self._pos3_eth_rx_03_perf_ns = None
            self._pos3_eth_rx_03_tid = None
        if self.fixture_pos == MACHINE_BRIDGE_POS and msg == "0B":
            delivery_us = None
            if self._pos4_eth_rx_0b_perf_ns is not None:
                delivery_us = round((now - self._pos4_eth_rx_0b_perf_ns) / 1000.0, 1)
            self.event_log.emit(
                "scr_trunk_0b_delivery",
                msg=msg,
                tid=tid,
                queue_wait_us=round(queue_wait_us, 1),
                trunk_to_machine_us=delivery_us,
            )
            self._pos4_eth_rx_0b_perf_ns = None
            self._pos4_eth_rx_0b_tid = None

    def on_packet_complete(self, fields: dict[str, Any]) -> None:
        msg = fields.get("msg")
        tid = fields.get("tid")
        now = self._now()
        if self.fixture_pos == MACHINE_BRIDGE_POS and msg == "03":
            self._pos4_native_03_perf_ns = now
            self._pos4_native_03_tid = tid
        if self.fixture_pos == TRUNK_BRIDGE_POS and msg == "0B":
            if self._last_scr_trunk_inject_perf_ns is not None:
                trunk_us = round((now - self._last_scr_trunk_inject_perf_ns) / 1000.0, 1)
                trunk_eth_us = None
                if self._pos3_eth_rx_0b_perf_ns is not None:
                    trunk_eth_us = round((now - self._pos3_eth_rx_0b_perf_ns) / 1000.0, 1)
                self.event_log.emit(
                    "scr_trunk_0b_capture",
                    msg=msg,
                    tid=tid,
                    trunk_reply_us=trunk_us,
                    inject_tid=self._last_scr_trunk_inject_tid,
                    trunk_eth_us=trunk_eth_us,
                )
                self._pos3_eth_rx_0b_perf_ns = None
                self._pos3_eth_rx_0b_tid = None
        self._maybe_post_inject_capture(
            window_bytes=int(fields.get("window_bytes") or 0),
            hs_window_us=fields.get("hs_window_us"),
            outcome="complete",
            hex_prefix=str(fields.get("hex_prefix") or ""),
            msg=msg,
        )

    def on_eth_tx(self, msg: str, tid: str | None) -> None:
        if self.fixture_pos != MACHINE_BRIDGE_POS or msg != "03":
            return
        if self._pos4_native_03_perf_ns is None:
            return
        now = self._now()
        self.event_log.emit(
            "scr_machine_03_eth",
            msg=msg,
            tid=tid,
            machine_to_eth_us=round((now - self._pos4_native_03_perf_ns) / 1000.0, 1),
            native_tid=self._pos4_native_03_tid,
        )
        self._pos4_native_03_perf_ns = None
        self._pos4_native_03_tid = None

    def on_window_skip(self, fields: dict[str, Any]) -> None:
        self._maybe_post_inject_capture(
            window_bytes=int(fields.get("window_bytes") or 0),
            hs_window_us=fields.get("hs_window_us"),
            outcome=str(fields.get("reason") or "skip"),
            hex_prefix=str(fields.get("hex_prefix") or ""),
        )

    def _maybe_post_inject_capture(
        self,
        *,
        window_bytes: int,
        hs_window_us: float | None,
        outcome: str,
        hex_prefix: str,
        msg: str | None = None,
    ) -> None:
        watch = self._post_inject
        if watch is None:
            return
        now = self._now()
        if (now - watch.inject_end_perf_ns) / 1000.0 > POST_INJECT_MAX_US:
            return
        if not hex_prefix and msg == "complete":
            hex_prefix = ""
        inject_prefix = _hex_prefix(watch.frame)
        probable_echo = bool(hex_prefix and inject_prefix and hex_prefix == inject_prefix)
        self.event_log.emit(
            "post_inject_capture",
            after_msg=watch.msg,
            after_len=len(watch.frame),
            after_tid=watch.tid,
            hs_window_us=hs_window_us,
            window_bytes=window_bytes,
            outcome=outcome,
            hex_prefix=hex_prefix,
            probable_echo=probable_echo,
            since_inject_us=round((now - watch.inject_end_perf_ns) / 1000.0, 1),
        )
        self._post_inject = None

    def _expire_post_inject(self, now: int) -> None:
        watch = self._post_inject
        if watch is None:
            return
        if (now - watch.inject_end_perf_ns) / 1000.0 <= POST_INJECT_MAX_US:
            return
        self.event_log.emit(
            "post_inject_capture",
            after_msg=watch.msg,
            after_len=len(watch.frame),
            after_tid=watch.tid,
            hs_window_us=None,
            window_bytes=0,
            outcome="timeout",
            hex_prefix="",
            probable_echo=False,
            since_inject_us=round((now - watch.inject_end_perf_ns) / 1000.0, 1),
        )
        self._post_inject = None

    @staticmethod
    def _now() -> int:
        import time

        return time.perf_counter_ns()


def patch_event_log_diagnostics(event_log: EventLog, diag: BridgeDiagnostics) -> None:
    """Emit post-inject / SCR events from window and packet outcomes."""
    original_emit = event_log.emit

    def emit(event: str, **fields: Any) -> None:
        if event == "window_skip":
            diag.on_window_skip(fields)
        elif event == "packet_complete":
            diag.on_packet_complete(fields)
        original_emit(event, **fields)

    event_log.emit = emit  # type: ignore[method-assign]
