"""Threaded TLS transport for Cthulhu Remote relay connections."""

from __future__ import annotations

import hashlib
import logging
import queue
import select
import socket
import ssl
import threading
import time
from collections.abc import Callable
from enum import Enum
from typing import Any

from gi.repository import GLib

from .connection_info import ConnectionInfo
from .protocol import PROTOCOL_VERSION, RemoteMessageType
from .serializer import JSONSerializer
from .socket_utils import host_port_to_address

logger = logging.getLogger(__name__)


class Action:
    """Small callback registrar used by the transport layer."""

    def __init__(self) -> None:
        self._handlers: list[Callable[..., Any]] = []
        self._lock = threading.Lock()

    def register(self, handler: Callable[..., Any]) -> None:
        with self._lock:
            if handler not in self._handlers:
                self._handlers.append(handler)

    def unregister(self, handler: Callable[..., Any]) -> None:
        with self._lock:
            if handler in self._handlers:
                self._handlers.remove(handler)

    def notify(self, **kwargs: Any) -> None:
        with self._lock:
            handlers = list(self._handlers)
        for handler in handlers:
            handler(**kwargs)


class RelayTransport:
    """Connect to an NVDA Remote compatible relay server over TLS."""

    def __init__(
        self,
        serializer: JSONSerializer,
        address: tuple[str, int],
        channel: str,
        connectionType: str,
        insecure: bool = False,
        timeout: int = 0,
        protocolVersion: int = PROTOCOL_VERSION,
    ) -> None:
        self.serializer = serializer
        self.address = address
        self.channel = channel
        self.connectionType = connectionType
        self.insecure = insecure
        self.timeout = timeout
        self.protocolVersion = protocolVersion
        self.connected = False
        self.closed = True
        self.successfulConnects = 0
        self.lastFailFingerprint: str | None = None
        self._buffer = b""
        self._socket: ssl.SSLSocket | None = None
        self._socketLock = threading.Lock()
        self._sendQueue: queue.Queue[bytes | None] = queue.Queue()
        self._sendThread: threading.Thread | None = None
        self._connectorThread: threading.Thread | None = None
        self._running = threading.Event()
        self._handlers: dict[RemoteMessageType, Action] = {}
        self.transportConnected = Action()
        self.transportDisconnected = Action()
        self.transportCertificateAuthenticationFailed = Action()
        self.transportConnectionFailed = Action()
        self.transportClosing = Action()
        self.transportConnected.register(self._send_join_messages)

    @classmethod
    def create(cls, connectionInfo: ConnectionInfo, serializer: JSONSerializer) -> "RelayTransport":
        return cls(
            serializer=serializer,
            address=(connectionInfo.hostname, connectionInfo.port),
            channel=connectionInfo.key,
            connectionType=connectionInfo.mode.value,
            insecure=connectionInfo.insecure,
        )

    def start(self) -> None:
        if self._connectorThread and self._connectorThread.is_alive():
            return
        self.closed = False
        self._running.set()
        self._connectorThread = threading.Thread(
            target=self._connector_loop,
            name="CthulhuRemoteConnector",
            daemon=True,
        )
        self._connectorThread.start()

    def close(self) -> None:
        self.transportClosing.notify()
        self.closed = True
        self._running.clear()
        self._disconnect()

    def register_inbound(self, messageType: RemoteMessageType, handler: Callable[..., Any]) -> None:
        self._handlers.setdefault(messageType, Action()).register(handler)

    def unregister_inbound(self, messageType: RemoteMessageType, handler: Callable[..., Any]) -> None:
        if messageType in self._handlers:
            self._handlers[messageType].unregister(handler)

    def send(self, messageType: str | Enum, **payload: Any) -> None:
        if not self.connected:
            logger.debug("Dropping remote message while disconnected: %r", messageType)
            return
        self._sendQueue.put(self.serializer.serialize(messageType=messageType, **payload))

    def _connector_loop(self) -> None:
        while self._running.is_set():
            try:
                self._run_once()
            except ssl.SSLCertVerificationError:
                logger.warning("Cthulhu Remote certificate verification failed")
            except OSError as error:
                logger.info("Cthulhu Remote connection failed: %s", error)
            except Exception:
                logger.exception("Cthulhu Remote connector failed")
            if self._running.is_set():
                time.sleep(5)

    def _run_once(self) -> None:
        try:
            self._socket = self._create_socket()
            self._socket.connect(self.address)
        except ssl.SSLCertVerificationError:
            self._capture_failed_fingerprint()
            self.transportCertificateAuthenticationFailed.notify()
            raise
        except Exception:
            self.transportConnectionFailed.notify()
            raise

        self.connected = True
        self.successfulConnects += 1
        self.transportConnected.notify()
        self._sendThread = threading.Thread(target=self._send_loop, name="CthulhuRemoteSend", daemon=True)
        self._sendThread.start()

        try:
            while self._running.is_set() and self._socket is not None:
                readers, _, errors = select.select([self._socket], [], [self._socket], 1.0)
                if errors:
                    break
                if readers:
                    self._read_available()
        finally:
            self.connected = False
            self.transportDisconnected.notify()
            self._disconnect()

    def _create_socket(self, insecure: bool | None = None) -> ssl.SSLSocket:
        insecure = self.insecure if insecure is None else insecure
        host, port = self.address
        family, socktype, proto, _, _ = socket.getaddrinfo(host, port)[0]
        plainSocket = socket.socket(family, socktype, proto)
        if self.timeout:
            plainSocket.settimeout(self.timeout)
        plainSocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        plainSocket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        context = ssl._create_unverified_context() if insecure else ssl.create_default_context()
        return context.wrap_socket(plainSocket, server_hostname=host)

    def _capture_failed_fingerprint(self) -> None:
        try:
            socketForCert = self._create_socket(insecure=True)
            socketForCert.connect(self.address)
            certificate = socketForCert.getpeercert(binary_form=True)
            socketForCert.close()
            if certificate:
                self.lastFailFingerprint = hashlib.sha256(certificate).hexdigest().lower()
        except Exception:
            self.lastFailFingerprint = None

    def _send_join_messages(self) -> None:
        self.send(RemoteMessageType.protocol_version, version=self.protocolVersion)
        self.send(RemoteMessageType.join, channel=self.channel, connection_type=self.connectionType)

    def _read_available(self) -> None:
        if self._socket is None:
            return
        with self._socketLock:
            self._socket.setblocking(False)
            try:
                data = self._buffer + self._socket.recv(16384)
            except ssl.SSLWantReadError:
                return
            finally:
                self._socket.setblocking(True)
        self._buffer = b""
        if not data:
            self._disconnect()
            return
        while b"\n" in data:
            line, _, data = data.partition(b"\n")
            self._parse_line(line)
        self._buffer = data

    def _parse_line(self, line: bytes) -> None:
        try:
            message = self.serializer.deserialize(line)
            messageType = RemoteMessageType(message.pop("type"))
        except Exception:
            logger.exception("Ignoring invalid remote message: %r", line)
            return
        action = self._handlers.get(messageType)
        if action is None:
            logger.debug("No handler registered for remote message type: %s", messageType.value)
            return
        GLib.idle_add(self._notify_action, action, message)

    def _notify_action(self, action: Action, payload: dict[str, Any]) -> bool:
        action.notify(**payload)
        return False

    def _send_loop(self) -> None:
        while True:
            item = self._sendQueue.get()
            if item is None:
                return
            try:
                with self._socketLock:
                    if self._socket is not None:
                        self._socket.sendall(item)
            except OSError:
                return

    def _disconnect(self) -> None:
        if self._sendThread is not None:
            self._sendQueue.put(None)
            self._sendThread.join(timeout=2)
            self._sendThread = None
        self._clear_send_queue()
        if self._socket is not None:
            try:
                self._socket.close()
            except OSError:
                pass
            self._socket = None
        self._buffer = b""

    def _clear_send_queue(self) -> None:
        try:
            while True:
                self._sendQueue.get_nowait()
        except queue.Empty:
            pass
