"""Server"""

# pylint: disable=C0209

import logging as _logging
import os as _os
import socket as _socket
import ssl as _ssl

import ser2tcp.connection_socket as _connection_socket
import ser2tcp.connection_ssl as _connection_ssl
import ser2tcp.connection_tcp as _connection_tcp
import ser2tcp.connection_telnet as _connection_telnet


class ConfigError(Exception):
    """Configuration error exception"""


class Server():
    """Server connection manager"""

    CONNECTIONS = {
        'TCP': _connection_tcp.ConnectionTcp,
        'TELNET': _connection_telnet.ConnectionTelnet,
        'SSL': _connection_ssl.ConnectionSsl,
        'SOCKET': _connection_socket.ConnectionSocket,
    }

    def __init__(self, config, ser, log=None):
        self._log = log if log else _logging.Logger(self.__class__.__name__)
        self._config = config
        self._serial = ser
        self._connections = []
        self._protocol = self._config['protocol'].upper()
        self._send_timeout = self._config.get('send_timeout')
        self._buffer_limit = self._config.get('buffer_limit')
        self._ssl_context = None
        self._socket = None
        if self._protocol not in self.CONNECTIONS:
            raise ConfigError('Unknown protocol %s' % self._protocol)
        if self._protocol == 'SOCKET':
            self._log.info(
                "  Server: %s %s",
                self._config['address'],
                self._protocol)
            self._socket = _socket.socket(
                _socket.AF_UNIX, _socket.SOCK_STREAM)
            sock_path = config['address']
            if _os.path.exists(sock_path):
                _os.unlink(sock_path)
            self._socket.bind(sock_path)
        else:
            self._log.info(
                "  Server: %s %d %s",
                self._config['address'],
                self._config['port'],
                self._protocol)
            if self._protocol == 'SSL':
                self._ssl_context = self._create_ssl_context()
            self._socket = _socket.socket(
                _socket.AF_INET, _socket.SOCK_STREAM, _socket.IPPROTO_TCP)
            self._socket.setsockopt(
                _socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
            self._socket.bind((config['address'], config['port']))
        self._socket.listen(1)

    def __del__(self):
        self.close()

    def _create_ssl_context(self):
        """Create SSL context from config"""
        ssl_config = self._config.get('ssl', {})
        certfile = ssl_config.get('certfile')
        keyfile = ssl_config.get('keyfile')
        ca_certs = ssl_config.get('ca_certs')
        if not certfile or not keyfile:
            raise ConfigError('SSL protocol requires certfile and keyfile')
        context = _ssl.SSLContext(_ssl.PROTOCOL_TLS_SERVER)
        context.load_cert_chain(certfile, keyfile)
        if ca_certs:
            context.load_verify_locations(ca_certs)
            context.verify_mode = _ssl.CERT_REQUIRED
        return context

    def _client_connect(self):
        """connect to client, will accept waiting connection"""
        sock, addr = self._socket.accept()
        if self._protocol == 'SOCKET':
            addr = (self._config['address'],)
        kwargs = {
            'connection': (sock, addr),
            'ser': self._serial,
            'send_timeout': self._send_timeout,
            'buffer_limit': self._buffer_limit,
            'log': self._log,
        }
        if self._ssl_context:
            kwargs['ssl_context'] = self._ssl_context
        try:
            connection = self.CONNECTIONS[self._protocol](**kwargs)
        except _connection_ssl.SslHandshakeError as err:
            self._log.info(
                "Client rejected: %s:%d (%s)", addr[0], addr[1], err)
            if not self._connections:
                self._serial.disconnect()
            return
        if self._serial.connect():
            self._connections.append(connection)
        else:
            connection.close()

    def close_connections(self):
        """close all clients"""
        while self._connections:
            self._connections.pop().close()

    def close(self):
        """Close socket and all connections"""
        if self._socket is not None:
            self.close_connections()
            self._socket.close()
            self._socket = None
            if self._protocol == 'SOCKET':
                sock_path = self._config['address']
                if _os.path.exists(sock_path):
                    _os.unlink(sock_path)

    def has_connections(self):
        """True if server has some connections"""
        return bool(self._connections)

    def read_sockets(self):
        """Return sockets for reading (server + all clients)"""
        sockets = [self._socket]
        for con in self._connections:
            sockets.append(con.socket())
        return sockets

    def write_sockets(self):
        """Return sockets for writing (clients with pending data)"""
        sockets = []
        for con in self._connections:
            if con.has_pending_data():
                sockets.append(con.socket())
        return sockets

    def _remove_connection(self, con):
        """Remove connection and disconnect serial if no connections left"""
        con.close()
        self._connections.remove(con)
        if not self._connections:
            self._serial.disconnect()

    def process_read(self, read_sockets):
        """Process sockets with read event"""
        if self._socket in read_sockets:
            self._client_connect()
        for con in list(self._connections):
            if con.socket() in read_sockets:
                data = b''
                try:
                    data = con.socket().recv(4096)
                    self._log.debug("(%s): %s", con.address_str(), data)
                except (ConnectionResetError, _ssl.SSLError) as err:
                    self._log.info("(%s): %s", con.address_str(), err)
                if not data:
                    self._remove_connection(con)
                    continue
                con.on_received(data)

    def process_write(self, write_sockets):
        """Process sockets with write event, flush buffers"""
        for con in list(self._connections):
            if con.socket() in write_sockets:
                result = con.flush()
                if result is None:
                    self._log.info(
                        "(%s): write error", con.address_str())
                    self._remove_connection(con)

    def process_stale(self):
        """Remove stale connections (send timeout expired)"""
        for con in list(self._connections):
            if con.is_stale():
                self._log.info(
                    "(%s): send timeout", con.address_str())
                self._remove_connection(con)

    def send(self, data):
        """Send data to connection"""
        for con in self._connections:
            con.send(data)
