from __future__ import annotations

import queue
import sys
import threading
import time
import uuid
from collections import deque
from typing import Deque, Dict, Iterable, Optional

from .can import CANError, CANFrame, CANInterface, ReadError, NullInterface, REPLInterface
from .actions import LockStrategy


class CANBusParticipant:
    def __init__(self) -> None:
        self.uuid = uuid.uuid4()
        self.lock_status = LockStrategy.none
        self.last_activity = 0.0
        self.frames_sent = 0

    @property
    def arbitration_ids(self) -> Iterable[int]:
        raise NotImplementedError

    def register(self, bus: "CANBusEmulation") -> None:
        return

    def unregister(self, bus: "CANBusEmulation") -> None:
        return

    def feed(self, frame: CANFrame, bus: "CANBusEmulation") -> None:
        return


class CANBusEmulation:
    def __init__(self, wires: CANInterface, timing_factor: float = 0.0) -> None:
        self.wires = wires
        self.participants: Dict[uuid.UUID, CANBusParticipant] = {}
        self._queues: Dict[uuid.UUID, queue.Queue] = {}
        self._threads: Dict[uuid.UUID, threading.Thread] = {}
        self.is_operating = False
        self._lock = threading.Lock()
        self._physical_io_enabled = True
        self._physical_io_lock = threading.Lock()
        self.last_activity = 0.0
        self.frames_seen = 0
        self._recent_frames: Deque[str] = deque(maxlen=8)
        self._activity_lock = threading.Lock()
        self.bitrate = 0
        self.timing_factor = max(0.0, timing_factor)
        self._simulate_bus_timing = isinstance(wires, (NullInterface, REPLInterface))

    def attach(self, participant: CANBusParticipant) -> None:
        with self._lock:
            self.participants[participant.uuid] = participant
            q: queue.Queue = queue.Queue()
            self._queues[participant.uuid] = q
            thread = threading.Thread(target=self._participant_worker, args=(participant, q), daemon=True)
            self._threads[participant.uuid] = thread
        participant.register(self)
        thread.start()

    def detach(self, participant: CANBusParticipant) -> None:
        participant_id = participant.uuid
        with self._lock:
            q = self._queues.pop(participant_id, None)
            thread = self._threads.pop(participant_id, None)
            participant_present = participant_id in self.participants
            if participant_present:
                self.participants.pop(participant_id, None)
        if q is not None:
            q.put(None)
        if thread is not None:
            thread.join(timeout=1.0)
        if participant_present:
            try:
                participant.unregister(self)
            except Exception:
                pass

    def _participant_worker(self, participant: CANBusParticipant, q: queue.Queue) -> None:
        while True:
            frame = q.get()
            if frame is None:
                break
            participant.feed(frame, self)
            q.task_done()

    def write(self, frame: CANFrame, originator: CANBusParticipant) -> None:
        if self._physical_io_enabled:
            try:
                self.wires.write(frame)
            except CANError as exc:
                self._disable_physical_io(str(exc))
        self._apply_bus_timing(frame)
        now = time.time()
        originator.last_activity = now
        originator.frames_sent += 1
        self._record_frame(frame, direction="Tx", timestamp=now)
        with self._lock:
            queues = list(self._queues.items())
        for participant_id, participant_queue in queues:
            if participant_id == originator.uuid:
                continue
            participant_queue.put(frame)

    def iterate(self) -> int:
        if not self._physical_io_enabled:
            return 0
        drained = 0
        while True:
            timeout_ms = 0 if drained else 1
            try:
                frame = self.wires.read(timeout_ms=timeout_ms)
            except ReadError:
                break
            now = time.time()
            self._record_frame(frame, direction="Rx", timestamp=now)
            self._apply_bus_timing(frame)
            with self._lock:
                queues = list(self._queues.values())
            for participant_queue in queues:
                participant_queue.put(frame)
            drained += 1
        return drained

    def operate(self, bitrate: int) -> None:
        if self.is_operating and self.wires.bitrate == bitrate:
            return
        if self.is_operating:
            self.wires.close()
        self.wires.open(bitrate)
        self.bitrate = bitrate
        self.is_operating = True

        def _loop() -> None:
            while self.is_operating:
                try:
                    drained = self.iterate()
                except CANError:
                    self.is_operating = False
                    break
                if drained == 0:
                    time.sleep(0.0005)

        thread = threading.Thread(target=_loop, daemon=True)
        thread.start()

    def lock_all_other_ecus(self, on_behalf_of: CANBusParticipant, strategy: LockStrategy) -> None:
        for participant in self.participants.values():
            if participant.uuid != on_behalf_of.uuid and participant.__class__.__name__ == "ECU":
                participant.lock_status = strategy

    def unlock_all_other_ecus(self, on_behalf_of: CANBusParticipant) -> None:
        for participant in self.participants.values():
            if participant.uuid != on_behalf_of.uuid and participant.__class__.__name__ == "ECU":
                participant.lock_status = LockStrategy.none

    def recent_frames(self) -> list[str]:
        with self._activity_lock:
            return list(self._recent_frames)

    def clear_recent_frames(self) -> None:
        with self._activity_lock:
            self._recent_frames.clear()

    def _record_frame(self, frame: CANFrame, direction: str, timestamp: float) -> None:
        with self._activity_lock:
            self.last_activity = timestamp
            self.frames_seen += 1
            payload = " ".join(f"{byte:02X}" for byte in frame.data[: frame.dlc])
            line = f"{direction} 0x{frame.arbitration_id:X} [{frame.dlc}] {payload}"
            self._recent_frames.append(line)

    def _disable_physical_io(self, reason: str) -> None:
        with self._physical_io_lock:
            if not self._physical_io_enabled:
                return
            self._physical_io_enabled = False
            try:
                self.wires.close()
            except Exception:
                pass
            print(
                f"Warning: Turning off physical CAN bus transmission due to error ({reason}). "
                "Continuing with simulated bus only.",
                file=sys.stderr,
            )
    
    def shutdown(self) -> None:
        """Gracefully shutdown the CAN bus emulation"""
        # Stop operation
        self.is_operating = False
        
        # Send None to all queues to signal threads to stop
        for q in self._queues.values():
            q.put(None)
        
        # Wait for threads to finish (with timeout)
        for thread in self._threads.values():
            thread.join(timeout=1.0)
        
        # Unregister all participants
        for participant in list(self.participants.values()):
            participant.unregister(self)
        
        # Clear collections
        self.participants.clear()
        self._queues.clear()
        self._threads.clear()
        
        # Close the CAN interface
        if hasattr(self.wires, 'close'):
            self.wires.close()

    def _apply_bus_timing(self, frame: CANFrame) -> None:
        if self.timing_factor <= 0.0 or not self._simulate_bus_timing or self.bitrate <= 0:
            return
        is_fd = bool(getattr(frame, "is_fd", False) or frame.dlc > 8)
        dlc_limit = 64 if is_fd else 8
        dlc = max(0, min(frame.dlc, dlc_limit))
        if is_fd:
            overhead = 85 if frame.arbitration_id > 0x7FF else 65
        else:
            overhead = 67 if frame.arbitration_id > 0x7FF else 47
        bits = overhead + (dlc * 8) + 3
        delay = (bits / self.bitrate) * self.timing_factor
        if delay > 0:
            time.sleep(delay)
