from __future__ import annotations

import dataclasses
import queue
import threading
import time
from dataclasses import dataclass, field
from typing import Callable, List, Optional, Sequence, Tuple, Any


@dataclass
class HandlerSnapshot:
    kind: str
    arbitration_id: int
    reply_id: int
    session_type: str | None
    security_level: int | None
    active_transfer: str | None
    memory_blocks: list[dict]
    routines: list[dict]
    identifiers: list[dict]
    ecu_identifiers: list[dict]
    local_identifiers: list[dict]


@dataclass
class ECUSnapshot:
    name: str
    lock_status: str
    last_activity: float
    handlers: List[HandlerSnapshot]


@dataclass
class BusSnapshot:
    timestamp: float
    last_activity: float
    recent_frames: List[str]
    ecus: List[ECUSnapshot]
    services: List["ServiceSnapshot"]


@dataclass
class ServiceSnapshot:
    name: str
    connected: bool
    clients: int
    last_rx: float | None
    last_tx: float | None
    rx_bytes: int = 0
    tx_bytes: int = 0
    tester_port: int | None = None
    broker_host: str | None = None
    broker_name: str | None = None
    listen_host: str | None = None
    listen_display_host: str | None = None
    listen_port: int | None = None
    listen_is_wildcard: bool = False
    error: str | None = None


@dataclass
class BusEvent:
    timestamp: float
    kind: str
    ecu: Optional[str] = None
    handler_key: Optional[str] = None
    payload: dict = field(default_factory=dict)


class StatusHub:
    def __init__(self, interval: float = 1.0, event_queue_size: int = 512) -> None:
        self.interval = interval
        self._sinks: List[Callable[[BusSnapshot], None]] = []
        self._event_sinks: List[Callable[[BusEvent], None]] = []
        self._event_queue: "queue.Queue[BusEvent]" = queue.Queue(maxsize=event_queue_size)
        self._event_thread: Optional[threading.Thread] = None
        self._running = False
        self._thread: threading.Thread | None = None
        self._bus = None
        self._services: Sequence[Tuple[str, Any]] = ()
        self._last_snapshot: Optional[BusSnapshot] = None
        self._drop_events = False
        self._dropped_event_count = 0

    def add_sink(self, sink: Callable[[BusSnapshot], None]) -> None:
        self._sinks.append(sink)

    def add_event_sink(self, sink: Callable[[BusEvent], None]) -> None:
        self._event_sinks.append(sink)

    def start(self, bus, services: Optional[Sequence[Tuple[str, Any]]] = None) -> None:
        if self._running:
            return
        self._bus = bus
        self._services = services or ()
        self._running = True
        self._thread = threading.Thread(target=self._loop, daemon=True)
        self._thread.start()
        if self._event_sinks and self._event_thread is None:
            self._event_thread = threading.Thread(target=self._event_loop, daemon=True)
            self._event_thread.start()

    def stop(self) -> None:
        self._running = False
        if self._event_thread:
            try:
                self._event_queue.put_nowait(BusEvent(timestamp=time.time(), kind="shutdown"))
            except queue.Full:
                pass

    def _loop(self) -> None:
        while self._running:
            snapshot = build_bus_snapshot(self._bus, services=self._services)
            for sink in self._sinks:
                sink(snapshot)
            self._emit_events(snapshot)
            time.sleep(self.interval)

    def _event_loop(self) -> None:
        while self._running:
            try:
                event = self._event_queue.get(timeout=0.5)
            except queue.Empty:
                continue
            if event.kind == "shutdown":
                return
            for sink in self._event_sinks:
                sink(event)

    def _emit_events(self, snapshot: BusSnapshot) -> None:
        if not self._event_sinks:
            self._last_snapshot = snapshot
            return
        if self._drop_events:
            if self._try_enqueue(BusEvent(timestamp=snapshot.timestamp, kind="resync", payload={"dropped": self._dropped_event_count})):
                self._drop_events = False
                self._dropped_event_count = 0

        events = diff_snapshots(self._last_snapshot, snapshot, activity_threshold=max(self.interval * 1.5, 0.5))
        for event in events:
            if not self._try_enqueue(event):
                self._drop_events = True
                self._dropped_event_count += 1
        self._last_snapshot = snapshot

    def _try_enqueue(self, event: BusEvent) -> bool:
        try:
            self._event_queue.put_nowait(event)
        except queue.Full:
            return False
        return True


def build_bus_snapshot(bus, services: Sequence[Tuple[str, Any]] | None = None) -> BusSnapshot:
    ecus = []
    bus_last_activity = getattr(bus, "last_activity", 0.0)
    recent_frames = []
    if hasattr(bus, "recent_frames"):
        recent_frames = bus.recent_frames()
    for participant in getattr(bus, "participants", {}).values():
        name = getattr(participant, "name", str(participant))
        lock_status = getattr(participant, "lock_status", None)
        if hasattr(lock_status, "value"):
            lock_status = lock_status.value
        handlers_snapshots: List[HandlerSnapshot] = []
        for handler in getattr(participant, "_handlers", []):
            session_type = getattr(handler, "current", None)
            if hasattr(session_type, "name"):
                session_type = session_type.name
            elif session_type is not None:
                session_type = str(session_type)
            security_level = getattr(handler, "security_level", None)
            active_transfer = getattr(handler, "active_transfer", None)
            active_transfer_name = getattr(active_transfer, "name", None) if active_transfer else None
            memory_blocks = []
            for block in getattr(handler, "memory", []) or []:
                state = "idle"
                if getattr(block, "has_incoming_transfer", False):
                    state = "rx"
                elif getattr(block, "has_outgoing_transfer", False):
                    state = "tx"
                elif getattr(block, "erased", False):
                    state = "erased"
                memory_blocks.append(
                    {
                        "name": getattr(block, "name", ""),
                        "address": getattr(block, "address", 0),
                        "size": getattr(block, "size", 0),
                        "mtu": getattr(block, "mtu", 0),
                        "state": state,
                        "next_block": getattr(block, "next_block", 0),
                    }
                )
            routines = []
            for routine in getattr(handler, "routines", {}).values():
                routines.append(
                    {
                        "id": getattr(routine, "identifier", 0),
                        "name": getattr(routine, "name", ""),
                        "security_levels": getattr(routine, "security_levels", []),
                        "responses": len(getattr(routine, "responses", []) or []),
                    }
                )
            identifiers = _snapshot_identifiers(getattr(handler, "identifiers", {}), scope="identifier")
            ecu_identifiers = _snapshot_identifiers(getattr(handler, "ecu_identifiers", {}), scope="ecu_identifier")
            local_identifiers = _snapshot_identifiers(getattr(handler, "local_identifiers", {}), scope="local_identifier")
            handlers_snapshots.append(
                HandlerSnapshot(
                    kind=type(handler).__name__,
                    arbitration_id=getattr(handler, "arbitration_id", 0),
                    reply_id=getattr(handler, "reply_id", 0),
                    session_type=session_type,
                    security_level=security_level,
                    active_transfer=active_transfer_name,
                    memory_blocks=memory_blocks,
                    routines=routines,
                    identifiers=identifiers,
                    ecu_identifiers=ecu_identifiers,
                    local_identifiers=local_identifiers,
                )
            )
        ecus.append(
            ECUSnapshot(
                name=name,
                lock_status=str(lock_status) if lock_status is not None else "unknown",
                last_activity=getattr(participant, "last_activity", 0.0),
                handlers=handlers_snapshots,
            )
        )
    return BusSnapshot(
        timestamp=time.time(),
        last_activity=bus_last_activity,
        recent_frames=recent_frames,
        ecus=ecus,
        services=_build_service_snapshots(services or ()),
    )


def _build_service_snapshots(services: Sequence[Tuple[str, Any]]) -> list[ServiceSnapshot]:
    snapshots: list[ServiceSnapshot] = []
    for name, service in services:
        data = {}
        if hasattr(service, "ui_snapshot"):
            try:
                data = service.ui_snapshot() or {}
            except Exception:
                data = {}
        connected = bool(data.get("connected", False))
        clients = int(data.get("clients", 0))
        last_rx = data.get("last_rx")
        last_tx = data.get("last_tx")
        tester_port_raw = data.get("tester_port")
        try:
            tester_port = int(tester_port_raw) if tester_port_raw is not None else None
        except (TypeError, ValueError):
            tester_port = None
        broker_host_raw = data.get("broker_host")
        broker_host = str(broker_host_raw).strip() if broker_host_raw is not None else None
        if not broker_host:
            broker_host = None
        broker_name_raw = data.get("broker_name")
        broker_name = str(broker_name_raw).strip() if broker_name_raw is not None else None
        if not broker_name:
            broker_name = None
        listen_host_raw = data.get("listen_host")
        listen_host = str(listen_host_raw).strip() if listen_host_raw is not None else None
        if not listen_host:
            listen_host = None
        listen_display_host_raw = data.get("listen_display_host")
        listen_display_host = (
            str(listen_display_host_raw).strip() if listen_display_host_raw is not None else None
        )
        if not listen_display_host:
            listen_display_host = None
        listen_port_raw = data.get("listen_port")
        try:
            listen_port = int(listen_port_raw) if listen_port_raw is not None else None
        except (TypeError, ValueError):
            listen_port = None
        error_raw = data.get("error")
        error = str(error_raw).strip() if error_raw is not None else None
        if not error:
            error = None
        snapshots.append(
            ServiceSnapshot(
                name=name,
                connected=connected,
                clients=clients,
                last_rx=last_rx,
                last_tx=last_tx,
                rx_bytes=int(data.get("rx_bytes", 0)),
                tx_bytes=int(data.get("tx_bytes", 0)),
                tester_port=tester_port,
                broker_host=broker_host,
                broker_name=broker_name,
                listen_host=listen_host,
                listen_display_host=listen_display_host,
                listen_port=listen_port,
                listen_is_wildcard=bool(data.get("listen_is_wildcard", False)),
                error=error,
            )
        )
    return snapshots


def _snapshot_identifiers(identifier_map, scope: str) -> list[dict]:
    output = []
    if isinstance(identifier_map, dict):
        items = identifier_map.items()
    else:
        items = []
    for identifier_id, identifier in sorted(items):
        data_type = getattr(identifier, "data_type", None)
        if hasattr(data_type, "value"):
            data_type_value = data_type.value
        elif hasattr(data_type, "name"):
            data_type_value = data_type.name
        elif data_type is not None:
            data_type_value = str(data_type)
        else:
            data_type_value = ""
        output.append(
            {
                "scope": scope,
                "id": int(identifier_id),
                "name": getattr(identifier, "name", ""),
                "data_type": data_type_value,
                "length": getattr(identifier, "length", 0),
            }
        )
    return output


def diff_snapshots(previous: Optional[BusSnapshot], current: BusSnapshot, activity_threshold: float) -> list[BusEvent]:
    events: list[BusEvent] = []
    if previous is None:
        for ecu in current.ecus:
            events.append(BusEvent(timestamp=current.timestamp, kind="ecu_added", ecu=ecu.name))
            for handler in ecu.handlers:
                events.append(
                    BusEvent(
                        timestamp=current.timestamp,
                        kind="handler_inventory",
                        ecu=ecu.name,
                        handler_key=_handler_key(handler),
                        payload=_handler_inventory_payload(handler),
                    )
                )
        events.append(
            BusEvent(
                timestamp=current.timestamp,
                kind="bus_state",
                payload={"active": _is_active(current.timestamp, current.last_activity, activity_threshold)},
            )
        )
        return events

    prev_ecus = {ecu.name: ecu for ecu in previous.ecus}
    curr_ecus = {ecu.name: ecu for ecu in current.ecus}

    for ecu_name in curr_ecus.keys() - prev_ecus.keys():
        events.append(BusEvent(timestamp=current.timestamp, kind="ecu_added", ecu=ecu_name))
    for ecu_name in prev_ecus.keys() - curr_ecus.keys():
        events.append(BusEvent(timestamp=current.timestamp, kind="ecu_removed", ecu=ecu_name))

    prev_bus_active = _is_active(previous.timestamp, previous.last_activity, activity_threshold)
    curr_bus_active = _is_active(current.timestamp, current.last_activity, activity_threshold)
    if prev_bus_active != curr_bus_active:
        events.append(BusEvent(timestamp=current.timestamp, kind="bus_state", payload={"active": curr_bus_active}))

    for ecu_name, ecu in curr_ecus.items():
        prev_ecu = prev_ecus.get(ecu_name)
        if prev_ecu is None:
            continue
        if ecu.lock_status != prev_ecu.lock_status:
            events.append(
                BusEvent(
                    timestamp=current.timestamp,
                    kind="ecu_state",
                    ecu=ecu_name,
                    payload={"lock_status": ecu.lock_status},
                )
            )
        prev_handlers = {_handler_key(handler): handler for handler in prev_ecu.handlers}
        curr_handlers = {_handler_key(handler): handler for handler in ecu.handlers}
        for handler_key in curr_handlers.keys() - prev_handlers.keys():
            handler = curr_handlers[handler_key]
            events.append(
                BusEvent(
                    timestamp=current.timestamp,
                    kind="handler_added",
                    ecu=ecu_name,
                    handler_key=handler_key,
                )
            )
            events.append(
                BusEvent(
                    timestamp=current.timestamp,
                    kind="handler_inventory",
                    ecu=ecu_name,
                    handler_key=handler_key,
                    payload=_handler_inventory_payload(handler),
                )
            )
        for handler_key in prev_handlers.keys() - curr_handlers.keys():
            events.append(
                BusEvent(
                    timestamp=current.timestamp,
                    kind="handler_removed",
                    ecu=ecu_name,
                    handler_key=handler_key,
                )
            )
        for handler_key, handler in curr_handlers.items():
            prev_handler = prev_handlers.get(handler_key)
            if prev_handler is None:
                continue
            if (
                handler.session_type != prev_handler.session_type
                or handler.security_level != prev_handler.security_level
                or handler.active_transfer != prev_handler.active_transfer
            ):
                events.append(
                    BusEvent(
                        timestamp=current.timestamp,
                        kind="handler_state",
                        ecu=ecu_name,
                        handler_key=handler_key,
                        payload={
                            "session_type": handler.session_type,
                            "security_level": handler.security_level,
                            "active_transfer": handler.active_transfer,
                        },
                    )
                )
            if _inventory_signature(handler) != _inventory_signature(prev_handler):
                events.append(
                    BusEvent(
                        timestamp=current.timestamp,
                        kind="handler_inventory",
                        ecu=ecu_name,
                        handler_key=handler_key,
                        payload=_handler_inventory_payload(handler),
                    )
                )
    return events


def _handler_key(handler: HandlerSnapshot) -> str:
    return f"{handler.kind}:{handler.arbitration_id:03X}->{handler.reply_id:03X}"


def _inventory_signature(handler: HandlerSnapshot) -> tuple:
    return (
        len(handler.routines),
        len(handler.identifiers),
        len(handler.ecu_identifiers),
        len(handler.local_identifiers),
        len(handler.memory_blocks),
    )


def _handler_inventory_payload(handler: HandlerSnapshot) -> dict:
    return {
        "routines": len(handler.routines),
        "identifiers": len(handler.identifiers),
        "ecu_identifiers": len(handler.ecu_identifiers),
        "local_identifiers": len(handler.local_identifiers),
        "memory_blocks": len(handler.memory_blocks),
    }


def _is_active(now: float, last_activity: float, threshold: float) -> bool:
    return now - last_activity <= threshold
