# 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 logging
import socket
from typing import Optional

from gi.repository import GLib

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

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: Optional[socket.socket] = 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:
                try:
                    self._socket.close()
                except Exception:
                    pass
                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:
            try:
                self._socket.shutdown(socket.SHUT_RDWR)
            except Exception:
                pass
        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:
            try:
                self._socket.close()
            except Exception:
                pass
            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
