# Copyright 2026, Jiri Eischmann <jiri@eischmann.cz> and the meshy contributors
# SPDX-License-Identifier: GPL-3.0-or-later

"""TCP transport for MeshCore companion devices over WiFi.

Uses the same frame format as USB serial.
"""

import contextlib
import logging
import socket

from gi.repository import GLib
from meshy.transport_base import (
    RX_MARKER,
    TX_MARKER,
    ConnectionState,
    FrameTransport,
)

log = logging.getLogger(__name__)

TCP_DEFAULT_PORT = 5000
TCP_CONNECT_TIMEOUT = 10  # seconds


class TcpDevice:
    """Represents a TCP endpoint for connection view compatibility."""

    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port
        self.path = f"{host}:{port}"
        self.address = self.path
        self.name = f"TCP {host}:{port}"

    def __repr__(self):
        return f"TcpDevice({self.path})"


class TcpTransport(FrameTransport):
    """TCP transport for MeshCore companion devices over WiFi."""

    _LABEL = "TCP"
    # Accept both markers — meshcore-proxy sends responses framed with 0x3C
    _RX_MARKERS = frozenset((RX_MARKER, TX_MARKER))

    def __init__(self):
        super().__init__()
        self._socket: socket.socket | None = None

    def get_paired_devices(self) -> list:
        """TCP doesn't have paired devices."""
        return []

    def connect_device(self, device_path: str):
        """Connect to a TCP endpoint. device_path is 'host:port'."""
        if self._state == ConnectionState.CONNECTED:
            self.disconnect()

        self._set_state(ConnectionState.CONNECTING)

        try:
            host, port_str = device_path.rsplit(":", 1)
            port = int(port_str)
        except (ValueError, AttributeError):
            log.error(f"Invalid TCP address: {device_path}")
            self._set_state(ConnectionState.DISCONNECTED)
            return

        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(TCP_CONNECT_TIMEOUT)
            sock.connect((host, port))
            sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
            sock.settimeout(None)  # blocking reads in thread
            self._socket = sock
            self._connected_address = device_path
            log.info(f"Connected to TCP {host}:{port}")

            self._start_read_thread()
            self._set_state(ConnectionState.CONNECTED)

        except Exception as e:
            log.error(f"TCP connect failed ({device_path}): {e}")
            if self._socket:
                with contextlib.suppress(Exception):
                    self._socket.close()
                self._socket = None
            self._set_state(ConnectionState.DISCONNECTED)

    def disconnect(self):
        """Disconnect from the TCP endpoint."""
        if self._state in (ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTING):
            return

        self._set_state(ConnectionState.DISCONNECTING)
        self._running = False

        if self._socket:
            with contextlib.suppress(Exception):
                self._socket.shutdown(socket.SHUT_RDWR)
        self._close_io()
        self._connected_address = None
        self._set_state(ConnectionState.DISCONNECTED)

    # ─── FrameTransport implementation ────────────────────────

    def _is_io_ready(self) -> bool:
        return self._socket is not None

    def _write_bytes(self, frame: bytes):
        self._socket.sendall(frame)

    def _close_io(self):
        if self._socket:
            with contextlib.suppress(Exception):
                self._socket.close()
            self._socket = None

    def _read_loop(self, gen: int):
        """Background thread that reads from the TCP socket."""
        sock = self._socket
        while self._running and gen == self._connect_gen and sock:
            try:
                data = sock.recv(4096)
                if not data:
                    if self._running and gen == self._connect_gen:
                        log.info("TCP: remote closed connection")
                        GLib.idle_add(self._handle_disconnect, gen)
                    break
                self._rx_buffer.extend(data)
                self._process_rx_buffer()
            except OSError as e:
                if self._running and gen == self._connect_gen:
                    log.error(f"TCP read error: {e}")
                    GLib.idle_add(self._handle_disconnect, gen)
                break
