from __future__ import annotations

import socket
import sys
import threading
import time
from typing import Optional, Tuple

from ..core.bus import CANBusEmulation, CANBusParticipant
from ..core.can import CANFrame, NullInterface, REPLInterface, SocketCANInterface
from cancorder_utils.canlogger_common import ClientManager, pack_frame
from cancorder_utils.zeroconf_service import LoggerZeroconfService

DEFAULT_LOGGER_PORT = 2518

def _describe_wires(bus: CANBusEmulation) -> Tuple[str, str]:
    wires = bus.wires
    if isinstance(wires, SocketCANInterface):
        return wires.backend, str(wires.channel)
    if isinstance(wires, REPLInterface):
        return "repl", "stdin"
    if isinstance(wires, NullInterface):
        return "virtual", "null"
    return wires.__class__.__name__.lower(), ""


class ECUconnectLoggerParticipant(CANBusParticipant):
    def __init__(self, server: "ECUconnectLoggerServer") -> None:
        super().__init__()
        self.server = server

    @property
    def arbitration_ids(self):
        return set()

    def feed(self, frame: CANFrame, bus: CANBusEmulation) -> None:
        self.server.handle_incoming_frame(frame)


class ECUconnectLoggerServer:
    def __init__(
        self,
        bus: CANBusEmulation,
        host: str = "0.0.0.0",
        port: int = DEFAULT_LOGGER_PORT,
        log: bool = False,
        service_name: Optional[str] = None,
    ) -> None:
        self.bus = bus
        self.host = host
        self.port = port
        self.log = log
        self.service_name = service_name
        self._socket: Optional[socket.socket] = None
        self._stop = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self._participant = ECUconnectLoggerParticipant(self)
        self._clients = ClientManager(logger=self._log if log else (lambda _msg: None))
        self._zeroconf: Optional[LoggerZeroconfService] = None
        self._last_rx: Optional[float] = None
        self._last_tx: Optional[float] = None
        self._rx_bytes = 0
        self._tx_bytes = 0

    def _log(self, message: str) -> None:
        if self.log:
            print(message)

    def start(self) -> bool:
        if self._thread and self._thread.is_alive():
            return True
        self._stop.clear()
        try:
            self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self._socket.settimeout(1.0)
            for attempt in range(10):
                try:
                    self._socket.bind((self.host, self.port))
                    break
                except OSError as exc:
                    self._log(f"ECUconnect-logger bind attempt {attempt + 1} failed: {exc}")
                    if attempt == 9:
                        return False
                    time.sleep(0.1)
            self._socket.listen(5)
            if self.port == 0:
                self.port = self._socket.getsockname()[1]
        except OSError as exc:
            self._log(f"ECUconnect-logger failed to bind {self.host}:{self.port}: {exc}")
            return False
        print(f"DEBUG: ECUconnect Logger TCP server started on {self.host}:{self.port}", flush=True)

        self.bus.attach(self._participant)
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

        self._start_zeroconf()
        return True

    def stop(self) -> None:
        self._stop.set()
        if hasattr(self._clients, "close_all"):
            self._clients.close_all()
        else:
            try:
                for sock in list(getattr(self._clients, "clients", {}).keys()):
                    try:
                        self._clients.remove_client(sock)
                    except Exception:
                        pass
            except Exception:
                pass
        if self._socket:
            try:
                self._socket.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                self._socket.close()
            except OSError:
                pass
            self._socket = None
        if self._zeroconf:
            self._zeroconf.stop()
            self._zeroconf = None
        if self._thread is not None:
            self._thread.join(timeout=1.0)
            self._thread = None
        print("DEBUG: ECUconnect Logger stopped", flush=True)

    def handle_incoming_frame(self, frame: CANFrame) -> None:
        if self._stop.is_set():
            return
        payload = bytes(frame.data[: frame.dlc])
        packet = pack_frame(
            int(time.time() * 1_000_000),
            frame.arbitration_id,
            frame.arbitration_id > 0x7FF,
            payload,
            is_fd=bool(getattr(frame, "is_fd", False) or frame.dlc > 8),
            bitrate_switch=bool(getattr(frame, "bitrate_switch", False)),
            error_state_indicator=bool(getattr(frame, "error_state_indicator", False)),
        )
        self._last_tx = time.time()
        self._tx_bytes += len(packet)
        self._clients.broadcast(packet)

    def _run(self) -> None:
        while not self._stop.is_set() and self._socket:
            try:
                client, addr = self._socket.accept()
            except socket.timeout:
                continue
            except OSError:
                break
            thread = threading.Thread(
                target=self._client_thread,
                args=(client, addr),
                daemon=True,
            )
            thread.start()

    def _client_thread(self, sock: socket.socket, addr: Tuple[str, int]) -> None:
        self._clients.add_client(sock, addr)
        try:
            sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
            while not self._stop.is_set():
                data = sock.recv(1024)
                if not data:
                    break
                self._last_rx = time.time()
                self._rx_bytes += len(data)
        except (OSError, ConnectionResetError):
            pass
        finally:
            self._clients.remove_client(sock)

    def _start_zeroconf(self) -> None:
        interface_name, channel = _describe_wires(self.bus)
        metadata = {
            "process": "ecumulator",
            "interface": interface_name,
            "channel": channel,
            "bitrate": str(self.bus.bitrate),
        }
        default_name = self.service_name or f"ECUmulator.py {socket.gethostname()}:{self.port}"
        buffered_logs: list[str] = []
        spinner_stop = threading.Event()

        def _spinner() -> None:
            frames = "|/-\\"
            idx = 0
            message = "Publishing Zeroconf"
            while not spinner_stop.is_set():
                frame = frames[idx % len(frames)]
                sys.stdout.write(f"\r{message} {frame}")
                sys.stdout.flush()
                idx += 1
                time.sleep(0.1)
            clear_len = len(message) + 2
            sys.stdout.write("\r" + (" " * clear_len) + "\r")
            sys.stdout.flush()

        spinner_thread = None
        if self.log:
            spinner_thread = threading.Thread(target=_spinner, daemon=True)
            spinner_thread.start()

            def _buffer_log(message: str) -> None:
                buffered_logs.append(message)
        else:
            _buffer_log = None  # type: ignore[assignment]

        self._zeroconf = LoggerZeroconfService(
            port=self.port,
            service_name=default_name,
            properties=metadata,
            logger=_buffer_log,
        )
        self._zeroconf.start()

        if spinner_thread:
            spinner_stop.set()
            spinner_thread.join(timeout=1.0)
            for line in buffered_logs:
                self._log(line)

    def ui_snapshot(self) -> dict:
        return {
            "connected": self._clients.client_count() > 0,
            "clients": self._clients.client_count(),
            "last_rx": self._last_rx,
            "last_tx": self._last_tx,
            "rx_bytes": self._rx_bytes,
            "tx_bytes": self._tx_bytes,
        }
