import asyncio
import os
import socket
import ssl
import tempfile
from logging import getLogger

from cpython.contextvars cimport *
from cpython.buffer cimport *
from cpython.bytes cimport *
from cpython.bytearray cimport *
from cpython.memoryview cimport *
from cpython.unicode cimport *

from . import constants
from .openssl_compat import find_openssl_library_paths
from .utils cimport *
from .openssl cimport *
from .transport cimport *


cdef init_openssl():
    cdef:
        bytes ssl_lib_name
        bytes crypto_lib_name
        const char* ssl_lib_ptr
        const char* crypto_lib_ptr
        const char* missing_lib

    ssl_lib_name, crypto_lib_name = find_openssl_library_paths()

    if init_openssl_compat(ssl_lib_name, crypto_lib_name) != 1:
        missing_lib = openssl_compat_last_error()
        if missing_lib != NULL:
            raise ImportError(
                f"aiofastnet: failed to initialize OpenSSL compatibility layer; "
                f"missing symbol: {PyUnicode_FromString(missing_lib)}; "
                f"ssl_lib={ssl_lib_name.decode()}, crypto_lib={crypto_lib_name.decode()}")
        raise ImportError("aiofastnet: failed to initialize OpenSSL compatibility layer")


init_openssl()

cdef:
    Py_ssize_t SSL_READ_BUFFER_SIZE = 128 * 1024
    Py_ssize_t SSL_WRITE_BUFFER_SIZE = 128 * 1024
    int SSL_OP_ALLOW_CLIENT_RENEGOTIATION = (1 << 8)
    int SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION = (1 << 18)

    # Number of seconds to wait for SSL handshake to complete
    # The default timeout matches that of Nginx.
    float SSL_HANDSHAKE_TIMEOUT = 60.0

    # Number of seconds to wait for SSL shutdown to complete
    # The default timeout mimics lingering_time
    float SSL_SHUTDOWN_TIMEOUT = 30.0


cpdef enum SSLProtocolState:
    UNWRAPPED = 0
    DO_HANDSHAKE = 1
    WRAPPED = 2
    FLUSHING = 3
    SHUTDOWN = 4



cdef enum AppProtocolState:
    # This tracks the state of app protocol (https://git.io/fj59P):
    #
    #     INIT -cm-> CON_MADE [-dr*->] [-er-> EOF?] -cl-> CON_LOST
    #
    # * cm: connection_made()
    # * dr: data_received()
    # * er: eof_received()
    # * cl: connection_lost()

    STATE_INIT = 0
    STATE_CON_MADE = 1
    STATE_EOF = 2
    STATE_CON_LOST = 3


cdef object _logger = getLogger('aiofastnet.ssl')


ctypedef struct PySSLContextHack:
    PyObject ob_base
    SSL_CTX *ctx


cdef SSL_CTX* _get_ssl_ctx_ptr(object py_ctx) except NULL:
    # Minimal runtime sanity check (still not foolproof)
    if not isinstance(py_ctx, ssl.SSLContext):
        raise TypeError("expected ssl.SSLContext")

    # A memory layout hack to extract SSL_CTX* ptr from python SSLContext object.
    #
    # I intentionally mirror ONLY the initial prefix of CPython's PySSLContext:
    # PyObject_HEAD + SSL_CTX *ctx
    #
    # This is NOT ABI-stable and may break across Python versions/build options.
    # I know it is ugly, but who cares, in some million years the sun will destroy
    # all life on earth, so everything is meaningless anyway.
    #
    # The guys from python are reluctant to expose it directly:
    # https://bugs.python.org/issue43902
    return (<PySSLContextHack*> <PyObject*> py_ctx).ctx


cdef int _print_error_cb(const char* str, size_t len, void* u) noexcept nogil:
    with gil:
        logger = <object><PyObject*>u
        err_str = PyUnicode_FromStringAndSize(str, len)
        logger.error(err_str)


cdef _log_error_queue():
    cdef void* u = <PyObject*>_logger
    ERR_print_errors_cb(&_print_error_cb, u)


cdef unsigned long _err_last_error():
    cdef unsigned long err_code = ERR_peek_last_error()
    ERR_clear_error()
    return err_code


cdef Py_ssize_t _bio_pending(BIO* bio):
    cdef int pending = BIO_pending(bio)
    if pending < 0:
        raise RuntimeError("unable to get pending len from BIO")
    return pending


cdef class SSLConnection:
    cdef:
        object ssl_ctx_py
        SSL_CTX* ssl_ctx
        bytearray incoming_buf
        bytearray outgoing_buf
        BIO* incoming
        BIO* outgoing
        SSL* ssl_object
        str server_hostname

    def __init__(self, ssl_context, bint is_server, str server_hostname):
        ERR_clear_error()

        self.ssl_ctx_py = ssl_context
        self.ssl_ctx = _get_ssl_ctx_ptr(ssl_context)

        self.incoming_buf = PyByteArray_FromStringAndSize(
            NULL, SSL_READ_BUFFER_SIZE)
        self.outgoing_buf = PyByteArray_FromStringAndSize(
            NULL, SSL_WRITE_BUFFER_SIZE)

        self.incoming = BIO_new_static_mem(
            PyByteArray_AS_STRING(self.incoming_buf),
            <size_t>PyByteArray_GET_SIZE(self.incoming_buf)
        )
        self.outgoing = BIO_new_static_mem(
            PyByteArray_AS_STRING(self.outgoing_buf),
            <size_t>PyByteArray_GET_SIZE(self.outgoing_buf)
        )

        self.ssl_object = SSL_new(self.ssl_ctx)
        self.server_hostname = server_hostname

        if self.incoming == NULL or self.outgoing == NULL or self.ssl_object == NULL:
            if self.incoming != NULL:
                BIO_free(self.incoming)
                self.incoming = NULL
            if self.outgoing != NULL:
                BIO_free(self.outgoing)
                self.outgoing = NULL
            if self.ssl_object != NULL:
                SSL_free(self.ssl_object)
                self.ssl_object = NULL
            raise MemoryError("Unable to initialize OpenSSL objects")

        if is_server:
            SSL_set_accept_state(self.ssl_object)
        else:
            SSL_set_connect_state(self.ssl_object)

        SSL_set_bio(self.ssl_object, self.incoming, self.outgoing)
        BIO_set_nbio(self.incoming, 1)
        BIO_set_nbio(self.outgoing, 1)

        cdef:
            X509_VERIFY_PARAM* ssl_verification_params
            X509_VERIFY_PARAM* ssl_ctx_verification_params
            unsigned int ssl_ctx_host_flags

        ssl_verification_params = SSL_get0_param(self.ssl_object)
        ssl_ctx_verification_params = SSL_CTX_get0_param(self.ssl_ctx)
        ssl_ctx_host_flags = X509_VERIFY_PARAM_get_hostflags(ssl_ctx_verification_params)
        X509_VERIFY_PARAM_set_hostflags(ssl_verification_params, ssl_ctx_host_flags)

        SSL_set_mode(self.ssl_object,
                     SSL_MODE_AUTO_RETRY | SSL_MODE_ENABLE_PARTIAL_WRITE)

        if self.server_hostname is not None:
            self._configure_hostname()

    def __dealloc__(self):
        # Free SSL and its BIO
        SSL_free(self.ssl_object)

    cdef inline tuple cipher(self):
        cdef const SSL_CIPHER* c = SSL_get_current_cipher(self.ssl_object)

        cdef const char* name = SSL_CIPHER_get_name(c)
        name_obj = PyUnicode_FromString(name) if name != NULL else None

        cdef const char* protocol = SSL_CIPHER_get_version(c)
        protocol_obj = PyUnicode_FromString(protocol) if name != NULL else None

        cdef int bits = SSL_CIPHER_get_bits(c, NULL)

        return (name_obj, protocol_obj, bits)

    cdef inline dict getpeercert(self):
        if SSL_is_init_finished(self.ssl_object) != 1:
            raise ssl.SSLError("SSL_is_init_finished failed")

        cdef X509* peer_cert = SSL_get_peer_certificate(self.ssl_object)
        if peer_cert == NULL:
            return None

        cdef int verification = self.ssl_ctx_py.verify_mode
        try:
            return self._decode_certificate(peer_cert) if verification & SSL_VERIFY_PEER else dict()
        finally:
            X509_free(peer_cert)

    # TODO: I don't think people would need this.
    # For now I return None but if somebody asks can be made compatible with
    # python implementation
    cdef inline str compression(self):
        return None

    cdef inline make_exc_from_ssl_error(self, str descr, int err_code):
        assert err_code != SSL_ERROR_NONE, "check logic"

        if err_code == SSL_ERROR_WANT_READ:
            return ssl.SSLWantReadError(descr)
        elif err_code == SSL_ERROR_WANT_WRITE:
            return ssl.SSLWantWriteError(descr)
        elif err_code == SSL_ERROR_ZERO_RETURN:
            return ssl.SSLZeroReturnError(descr)
        elif err_code == SSL_ERROR_SYSCALL:
            return ssl.SSLSyscallError(descr)
        elif err_code == SSL_ERROR_SSL:
            return self._exc_from_err_last_error(descr)
        else:
            return ssl.SSLError(f"{descr}, unknown error_code={err_code}")

    cdef inline _exc_from_err_last_error(self, str descr):
        cdef unsigned long last_error = _err_last_error()
        cdef int lib = ERR_GET_LIB(last_error)
        cdef const char * lib_ptr
        cdef const char * reason_ptr
        cdef const char * verify_ptr

        _log_error_queue()

        lib_ptr = ERR_lib_error_string(last_error)
        lib_name = PyUnicode_FromString(lib_ptr) if lib_ptr != NULL else f"UNKNOWN_{lib}"
        lib_name = lib_name.upper()
        reason_ptr = ERR_reason_error_string(last_error)
        reason_name = PyUnicode_FromString(
            reason_ptr) if reason_ptr != NULL else ""
        reason_name = reason_name.upper().replace(" ", "_")

        if reason_name == "CERTIFICATE_VERIFY_FAILED":
            assert self.server_hostname is not None
            verify_code = SSL_get_verify_result(self.ssl_object)
            verify_ptr = X509_verify_cert_error_string(verify_code)
            txt = PyUnicode_FromString(verify_ptr) if verify_ptr != NULL else ""
            str_error = f"[{lib_name}: {reason_name}] {descr}: {txt}"
            exc = ssl.SSLCertVerificationError()
            exc.verify_code = verify_code
            exc.verify_message = txt
        else:
            str_error = f"[{lib_name}: {reason_name}] {descr}"
            exc = ssl.SSLError()
        exc.strerror = str_error
        exc.library = lib_name
        exc.reason = reason_name
        return exc

    cdef inline _configure_hostname(self):
        if not self.server_hostname or self.server_hostname.startswith("."):
            raise ValueError("server_hostname cannot be an empty string or start with a leading dot.")

        cdef bytes server_hostname_b = self.server_hostname.encode()
        cdef char* server_hostname_ptr = PyBytes_AS_STRING(server_hostname_b)

        cdef ASN1_OCTET_STRING* ip = a2i_IPADDRESS(PyBytes_AS_STRING(server_hostname_b))
        if ip == NULL:
            ERR_clear_error()

        cdef X509_VERIFY_PARAM* ssl_verification_params
        try:
            # Only send SNI extension for non-IP hostnames
            if ip == NULL:
                if not SSL_set_tlsext_host_name(self.ssl_object, server_hostname_ptr):
                    _log_error_queue()
                    ERR_clear_error()
                    raise ssl.SSLError("SSL_set_tlsext_host_name failed")

            if self.ssl_ctx_py.check_hostname:
                ssl_verification_params = SSL_get0_param(self.ssl_object)
                if ip == NULL:
                    if not X509_VERIFY_PARAM_set1_host(ssl_verification_params, server_hostname_ptr, len(server_hostname_b)):
                        raise ssl.SSLError("X509_VERIFY_PARAM_set1_host failed")
                else:
                    if not X509_VERIFY_PARAM_set1_ip(ssl_verification_params, ASN1_STRING_get0_data(ip), ASN1_STRING_length(ip)):
                        raise ssl.SSLError("X509_VERIFY_PARAM_set1_host failed")
        finally:
            if ip != NULL:
                ASN1_OCTET_STRING_free(ip)

    cdef inline _decode_certificate(self, X509* certificate):
        cdef int der_len = i2d_X509(certificate, NULL)
        cdef bytes der
        cdef unsigned char* p
        cdef str path = ""

        if der_len <= 0:
            raise ssl.SSLError("i2d_X509 failed")

        der = PyBytes_FromStringAndSize(NULL, der_len)
        if der is None:
            raise MemoryError()

        p = <unsigned char*>PyBytes_AS_STRING(der)
        if i2d_X509(certificate, &p) != der_len:
            raise ssl.SSLError("i2d_X509 produced invalid DER size")

        pem = ssl.DER_cert_to_PEM_cert(der)
        tmp = tempfile.NamedTemporaryFile(mode="w", delete=False, encoding="ascii")
        try:
            tmp.write(pem)
            tmp.close()
            path = tmp.name
            return ssl._ssl._test_decode_cert(path)
        finally:
            if path:
                os.unlink(path)


cdef inline _create_transport_context(server_side, server_hostname):
    # Client side may pass ssl=True to use a default
    # context; in that case the sslcontext passed is None.
    # The default is secure for client connections.
    # Python 3.4+: use up-to-date strong settings.
    sslcontext = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
    if not server_hostname:
        sslcontext.check_hostname = False
    return sslcontext


cdef class SSLTransport(Transport):
    cdef:
        SSLProtocol _ssl_protocol

    def __init__(self, SSLProtocol ssl_protocol):
        self._ssl_protocol = ssl_protocol

    def get_extra_info(self, name, default=None):
        return self._ssl_protocol._get_extra_info(name, default)

    def set_protocol(self, protocol):
        self._ssl_protocol._set_app_protocol(protocol)

    def get_protocol(self):
        return self._ssl_protocol._get_app_protocol()

    def is_reading(self):
        return self._ssl_protocol._get_tcp_transport().is_reading()

    def pause_reading(self):
        """Pause the receiving end.

        No data will be passed to the protocol's data_received()
        method until resume_reading() is called.
        """
        self._ssl_protocol.pause_reading()

    def resume_reading(self):
        """Resume the receiving end.

        Data received will once again be passed to the protocol's
        data_received() method.
        """
        self._ssl_protocol.resume_reading()

    def set_write_buffer_limits(self, high=None, low=None):
        """Set the high- and low-water limits for write flow control.

        These two values control when to call the protocol's
        pause_writing() and resume_writing() methods.  If specified,
        the low-water limit must be less than or equal to the
        high-water limit.  Neither value can be negative.

        The defaults are implementation-specific.  If only the
        high-water limit is given, the low-water limit defaults to an
        implementation-specific value less than or equal to the
        high-water limit.  Setting high to zero forces low to zero as
        well, and causes pause_writing() to be called whenever the
        buffer becomes non-empty.  Setting low to zero causes
        resume_writing() to be called only once the buffer is empty.
        Use of zero for either limit is generally sub-optimal as it
        reduces opportunities for doing I/O and computation
        concurrently.
        """
        self._ssl_protocol._get_tcp_transport().set_write_buffer_limits(high, low)

    def get_write_buffer_limits(self):
        return self._ssl_protocol._get_tcp_transport().get_write_buffer_limits()

    def get_write_buffer_size(self):
        """Return the current size of the write buffers."""
        return self._ssl_protocol._get_tcp_transport().get_write_buffer_size()

    cpdef write(self, data):
        """Write some data bytes to the transport.

        This does not block; it buffers the data and arranges for it
        to be sent out asynchronously.
        """
        self._ssl_protocol.write(data)

    cpdef writelines(self, list_of_data):
        """Write a list (or any iterable) of data bytes to the transport.

        The default implementation concatenates the arguments and
        calls write() on the result.
        """
        self._ssl_protocol.writelines(list_of_data)

    cdef write_mem(self, char* ptr, Py_ssize_t sz):
        self._ssl_protocol.write_mem(ptr, sz)

    def write_eof(self):
        """Close the write end after flushing buffered data.

        This raises :exc:`NotImplementedError` right now.
        """
        raise NotImplementedError()

    def can_write_eof(self):
        """Return True if this transport supports write_eof(), False if not."""
        return False

    def is_closing(self):
        return self._ssl_protocol._is_closing()

    def close(self):
        """Close the transport.

        Buffered data will be flushed asynchronously.  No more data
        will be received.  After all buffered data is flushed, the
        protocol's connection_lost() method will (eventually) called
        with None as its argument.
        """
        self._ssl_protocol._start_shutdown()

    def abort(self):
        """Close the transport immediately.

        Buffered data will be lost.  No more data will be received.
        The protocol's connection_lost() method will (eventually) be
        called with None as its argument.
        """
        self._ssl_protocol._abort(None)


cdef class SSLProtocol(Protocol):
    """SSL protocol.

    Implementation of SSL on top of a socket using incoming and outgoing
    buffers which are ssl.MemoryBIO objects.
    """

    cdef:
        bint _server_side
        str _server_hostname
        object _sslcontext
        SSLConnection _ssl_connection

        dict _extra
        list _write_backlog

        object _loop
        SSLTransport _app_transport

        Transport _transport
        object _ssl_handshake_timeout
        object _ssl_shutdown_timeout
        object _ssl_handshake_complete_waiter

        SSLProtocolState _state
        size_t _conn_lost
        AppProtocolState _app_state

        object _app_protocol
        bint _app_protocol_is_buffered
        bint _app_protocol_aiofn

        object _handshake_start_time
        object _handshake_timeout_handle
        object _shutdown_timeout_handle

        bint _reading_paused
        bint _is_debug

    def __init__(self,
                 loop,
                 app_protocol,
                 sslcontext,
                 ssl_handshake_complete_waiter=None,
                 server_side=False, server_hostname=None,
                 call_connection_made=True,
                 ssl_handshake_timeout=None,
                 ssl_shutdown_timeout=None):
        if ssl_handshake_timeout is None:
            ssl_handshake_timeout = SSL_HANDSHAKE_TIMEOUT
        elif ssl_handshake_timeout <= 0:
            raise ValueError(
                f"ssl_handshake_timeout should be a positive number, "
                f"got {ssl_handshake_timeout}")
        if ssl_shutdown_timeout is None:
            ssl_shutdown_timeout = SSL_SHUTDOWN_TIMEOUT
        elif ssl_shutdown_timeout <= 0:
            raise ValueError(
                f"ssl_shutdown_timeout should be a positive number, "
                f"got {ssl_shutdown_timeout}")

        if server_side and not sslcontext:
            raise ValueError('Server side SSL needs a valid SSLContext')

        if not sslcontext or sslcontext == True:
            sslcontext = _create_transport_context(server_side, server_hostname)

        self._server_side = server_side
        self._server_hostname = None if server_side else server_hostname
        self._sslcontext = sslcontext
        self._ssl_connection = None
        # SSL-specific extra info. More info are set when the handshake
        # completes.
        self._extra = dict(sslcontext=sslcontext)

        # App data write buffering
        self._write_backlog = []

        self._loop = loop
        self._set_app_protocol(app_protocol)
        self._app_transport = None
        # transport, ex: SelectorSocketTransport
        self._transport = None
        self._ssl_handshake_timeout = ssl_handshake_timeout
        self._ssl_shutdown_timeout = ssl_shutdown_timeout
        self._ssl_handshake_complete_waiter = ssl_handshake_complete_waiter

        self._state = UNWRAPPED
        self._conn_lost = 0  # Set when connection_lost called
        if call_connection_made:
            self._app_state = STATE_INIT
        else:
            self._app_state = STATE_CON_MADE
        self._reading_paused = False
        self._is_debug = loop.get_debug()

    def __repr__(self):
        info = [self.__class__.__name__]
        if self._server_side:
            info.append("server")
        else:
            info.append("client")
        if self._transport is not None:
            sock: socket.socket = self._transport.get_extra_info("socket")
            info.append(f"fd={sock.fileno()}")

        wbuf_size = self.get_local_write_buffer_size()
        info.append(f', wbuf_size={wbuf_size}>')
        return '<{}>'.format(' '.join(info))

    cpdef is_buffered_protocol(self):
        return True

    cpdef _set_app_protocol(self, app_protocol):
        self._app_protocol = app_protocol
        self._app_protocol_is_buffered = aiofn_is_buffered_protocol(app_protocol)
        self._app_protocol_aiofn = isinstance(app_protocol, Protocol)

    cpdef _get_app_protocol(self):
        return self._app_protocol

    cpdef get_app_transport(self):
        if self._app_transport is None:
            self._app_transport = SSLTransport(self)
        return self._app_transport

    cdef inline Transport _get_tcp_transport(self):
        return self._transport

    def connection_made(self, transport):
        """Called when the low-level connection is made.

        Start the SSL handshake.
        """
        self._transport = transport
        self._start_handshake()

    def connection_lost(self, exc):
        """Called when the low-level connection is lost or closed.

        The argument is an exception object or None (the latter
        meaning a regular EOF is received or the connection was
        aborted or closed).
        """
        self._write_backlog.clear()

        # I don't know if we really need this
        BIO_reset(self._ssl_connection.outgoing)

        self._conn_lost += 1

        if self._state != DO_HANDSHAKE:
            if self._app_state == STATE_CON_MADE or \
                    self._app_state == STATE_EOF:
                self._app_state = STATE_CON_LOST
                self._loop.call_soon(self._app_protocol.connection_lost, exc)
        self._set_state(UNWRAPPED)
        self._transport = None

        # Decrease ref counters to user instances to avoid cyclic references
        # between user protocol, SSLProtocol and SSLTransport.
        # This helps to deallocate useless objects asap.
        # If not done then some tests like test_create_connection_memory_leak
        # will fail.
        self._app_transport = None
        self._app_protocol = None
        self._wakeup_waiter(exc)

        if self._shutdown_timeout_handle:
            self._shutdown_timeout_handle.cancel()
            self._shutdown_timeout_handle = None
        if self._handshake_timeout_handle:
            self._handshake_timeout_handle.cancel()
            self._handshake_timeout_handle = None

    cpdef get_buffer(self, Py_ssize_t n):
        cdef char* buf_ptr
        cdef size_t buf_len
        cdef int rc

        rc = BIO_static_mem_get_write_buf(
            self._ssl_connection.incoming, &buf_ptr, &buf_len)
        if rc != 1:
            raise RuntimeError("incoming BIO: unable to get writable buffer")
        if buf_len == 0:
            raise RuntimeError("incoming BIO: no writable capacity")

        return PyMemoryView_FromMemory(buf_ptr, <Py_ssize_t>buf_len, PyBUF_WRITE)

    cpdef buffer_updated(self, Py_ssize_t nbytes):
        if BIO_static_mem_produce(
                self._ssl_connection.incoming,
                <size_t>nbytes) != 1:
            raise RuntimeError("incoming BIO: unable to publish received bytes")

        if self._is_debug:
            _logger.debug("%r: buffer_updated(%d)", self, nbytes)

        if self._state == DO_HANDSHAKE:
            self._do_handshake()

        elif self._state == WRAPPED:
            self._do_read()

        elif self._state == FLUSHING:
            self._do_flush()

        elif self._state == SHUTDOWN:
            self._do_shutdown()

    # Underlying transport use this to take into account upstream write buffer
    # size when deciding to report pause_writing()/resume_writing()
    cpdef Py_ssize_t get_local_write_buffer_size(self) except -1:
        cdef Py_ssize_t total = 0
        for data in self._write_backlog:
            total += len(data)

        if self._app_protocol_aiofn:
            total += (<Protocol> self._app_protocol).get_local_write_buffer_size()

        if self._ssl_connection is not None:
            total += _bio_pending(self._ssl_connection.outgoing)

        return total

    def eof_received(self):
        """Called when the other end of the low-level stream
        is half-closed.

        If this returns a false value (including None), the transport
        will close itself.  If it returns a true value, closing the
        transport is up to the protocol.
        """
        try:
            if self._is_debug:
                _logger.debug("%r received EOF", self)

            if self._state == DO_HANDSHAKE:
                self._on_handshake_complete(ConnectionResetError)

            elif self._state == WRAPPED or self._state == FLUSHING:
                # We treat a low-level EOF as a critical situation similar to a
                # broken connection - just send whatever is in the buffer and
                # close. No application level eof_received() is called -
                # because we don't want the user to think that this is a
                # graceful shutdown triggered by SSL "close_notify".
                self._set_state(SHUTDOWN)
                self._on_shutdown_complete(None)

            elif self._state == SHUTDOWN:
                self._on_shutdown_complete(None)

        except Exception:
            self._transport.close()
            raise

    cdef inline _wakeup_waiter(self, exc=None):
        if (self._ssl_handshake_complete_waiter is not None and
                not self._ssl_handshake_complete_waiter.done()):
            if exc is not None:
                self._ssl_handshake_complete_waiter.set_exception(exc)
            else:
                self._ssl_handshake_complete_waiter.set_result(None)

    cdef inline _get_extra_info(self, name, default=None):
        if name == "ssl_object":
            return self._ssl_connection
        elif name == "ssl_protocol":
            return self
        elif name in self._extra:
            return self._extra[name]
        elif self._transport is not None:
            return self._transport.get_extra_info(name, default)
        else:
            return default

    cdef inline _set_state(self, SSLProtocolState new_state):
        cdef bint allowed = False

        if self._is_debug:
            _logger.debug("%s: change state to %s", self, SSLProtocolState(new_state).name)

        if new_state == UNWRAPPED:
            allowed = True

        elif self._state == UNWRAPPED and new_state == DO_HANDSHAKE:
            allowed = True

        elif self._state == DO_HANDSHAKE and new_state == WRAPPED:
            allowed = True

        # User requested re-negotiate
        elif self._state == WRAPPED and new_state == DO_HANDSHAKE:
            allowed = True

        elif self._state == WRAPPED and new_state == FLUSHING:
            allowed = True

        elif self._state == WRAPPED and new_state == SHUTDOWN:
            allowed = True

        elif self._state == FLUSHING and new_state == SHUTDOWN:
            allowed = True

        if allowed:
            self._state = new_state

        else:
            raise RuntimeError(
                'cannot switch state from {} to {}'.format(
                    self._state, new_state))

    # Handshake flow

    cdef inline _start_handshake(self):
        if self._is_debug:
            _logger.debug("%r: starts SSL handshake", self)
            self._handshake_start_time = self._loop.time()
        else:
            self._handshake_start_time = None

        self._set_state(DO_HANDSHAKE)

        # start handshake timeout count down
        self._handshake_timeout_handle = \
            self._loop.call_later(self._ssl_handshake_timeout,
                                  self._check_handshake_timeout)

        try:
            self._ssl_connection = SSLConnection(
                self._sslcontext,
                self._server_side,
                self._server_hostname
            )
        except Exception as ex:
            self._on_handshake_complete(ex)
        else:
            self._do_handshake()

    cdef inline _check_handshake_timeout(self):
        if self._state == DO_HANDSHAKE:
            msg = (
                f"SSL handshake is taking longer than "
                f"{self._ssl_handshake_timeout} seconds: "
                f"aborting the connection"
            )
            self._fatal_error(ConnectionAbortedError(msg))

    cdef inline _do_handshake(self):
        cdef:
            int rc
            int ssl_error

        while True:
            rc = SSL_do_handshake(self._ssl_connection.ssl_object)
            if rc == 1:
                if self._is_debug:
                    _logger.debug("%r: SSL_do_handshake() = %d", self, rc)
                self._on_handshake_complete(None)
                self._maybe_send_outgoing(True)
                return

            # Since our outgoing bio has limited capacity we may get
            # SSL_ERROR_WANT_WRITE. Handshake does not need much space, but for
            # correctness-sake we need flush and re-try
            ssl_error = SSL_get_error(self._ssl_connection.ssl_object, rc)
            if self._is_debug:
                _logger.debug("%r: SSL_do_handshake() = %d, ssl_error=%d", self, rc, ssl_error)

            if ssl_error == SSL_ERROR_WANT_WRITE:
                self._maybe_send_outgoing(True)
                continue

            if ssl_error == SSL_ERROR_WANT_READ:
                self._maybe_send_outgoing(True)
                return

            self._on_handshake_complete(self._ssl_connection.make_exc_from_ssl_error("ssl handshake failed", ssl_error))
            return

    cdef inline _on_handshake_complete(self, handshake_exc):
        if self._handshake_timeout_handle is not None:
            self._handshake_timeout_handle.cancel()
            self._handshake_timeout_handle = None

        try:
            if handshake_exc is None:
                self._set_state(WRAPPED)
            else:
                raise handshake_exc
        except Exception as exc:
            self._set_state(UNWRAPPED)
            if isinstance(exc, ssl.CertificateError):
                msg = 'SSL handshake failed on verifying the certificate'
            else:
                msg = 'SSL handshake failed'
            self._fatal_error(exc, msg)
            self._wakeup_waiter(exc)
            return

        if self._is_debug:
            dt = self._loop.time() - self._handshake_start_time
            _logger.debug("%r: SSL handshake took %.1f ms", self, dt * 1e3)

        # Add extra info that becomes available after handshake.
        # TODO: add compression
        self._extra.update(
            peercert=self._ssl_connection.getpeercert(),
            cipher=self._ssl_connection.cipher(),
            compression=self._ssl_connection.compression()
        )
        if self._app_state == STATE_INIT:
            self._app_state = STATE_CON_MADE
            try:
                self._app_protocol.connection_made(self.get_app_transport())
            except (SystemExit, KeyboardInterrupt):
                raise
            except Exception as exc:
                self._fatal_error_no_close(exc, "user connection_made raised an exception")
        self._wakeup_waiter()

        # We should wakeup user code before sending the first data below. In
        # case of `start_tls()`, the user can only get the SSLTransport in the
        # wakeup callback, because `connection_made()` is not called again.
        # We should schedule the first data later than the wakeup callback so
        # that the user get a chance to e.g. check ALPN with the transport
        # before having to handle the first data.
        self._loop.call_soon(self._do_read)

    # Shutdown flow

    cdef inline bint _is_closing(self) noexcept:
        return self._state in (FLUSHING, SHUTDOWN, UNWRAPPED)

    cdef inline _start_shutdown(self):
        if self._state in (FLUSHING, SHUTDOWN, UNWRAPPED):
            return
        # we don't need the context for _abort or the timeout, because
        # TCP transport._force_close() should be able to call
        # connection_lost() in the right context
        if self._state == DO_HANDSHAKE:
            self._abort(None)
        else:
            self._set_state(FLUSHING)
            self._shutdown_timeout_handle = \
                self._loop.call_later(self._ssl_shutdown_timeout,
                                      lambda: self._check_shutdown_timeout())
            self._do_flush()

    cdef inline _check_shutdown_timeout(self):
        if self._state in (FLUSHING, SHUTDOWN):
            self._abort(asyncio.TimeoutError('SSL shutdown timed out'))

    cdef inline _do_read_into_void(self):
        """Consume and discard incoming application data.

        If close_notify is received for the first time, call eof_received.
        """
        cdef:
            bytearray buffer = PyByteArray_FromStringAndSize(NULL, 16*1024)
            size_t bytes_read
            int rc = 1
        while rc == 1:
            rc = SSL_read_ex(self._ssl_connection.ssl_object,
                             PyByteArray_AS_STRING(buffer),
                             PyByteArray_GET_SIZE(buffer),
                             &bytes_read)

        cdef int ssl_error = SSL_get_error(self._ssl_connection.ssl_object, rc)
        if ssl_error in (SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE):
            return

        if ssl_error == SSL_ERROR_ZERO_RETURN:
            self._call_eof_received()
            return

        raise self._ssl_connection.make_exc_from_ssl_error("SSL_read_ex failed", ssl_error)

    cdef inline _do_flush(self):
        """Flush the write backlog, discarding new data received.

        We don't send close_notify in FLUSHING because we still want to send
        the remaining data over SSL, even if we received a close_notify. Also,
        no application-level resume_writing() or pause_writing() will be called
        in FLUSHING, as we could fully manage the flow control internally.
        """
        try:
            self._do_read_into_void()
            self._flush_write_backlog()
        except Exception as ex:
            self._on_shutdown_complete(ex)
        else:
            if not self.get_local_write_buffer_size():
                self._set_state(SHUTDOWN)
                self._do_shutdown()

    cdef inline _do_shutdown(self):
        """Send close_notify and wait for the same from the peer."""
        cdef:
            int rc
            int err_code

        try:
            # we must skip all application data (if any) before unwrap
            self._do_read_into_void()

            while True:
                rc = SSL_shutdown(self._ssl_connection.ssl_object)
                if rc == 1:
                    self._on_shutdown_complete(None)
                    return

                # From openssl docs
                # Unlike most other function, returning 0 does not indicate an
                # error. SSL_get_error(3) should not get called, it may
                # misleadingly indicate an error even though no error occurred.
                if rc == 0:
                    self._maybe_send_outgoing(True)
                    return

                err_code = SSL_get_error(self._ssl_connection.ssl_object, rc)

                # Re-try shutdown because outgoing bio has no space left
                if err_code == SSL_ERROR_WANT_WRITE:
                    self._maybe_send_outgoing(True)
                    continue

                if err_code == SSL_ERROR_WANT_READ:
                    self._maybe_send_outgoing(True)
                    return

                raise self._ssl_connection.make_exc_from_ssl_error("SSL_shutdown failed", err_code)
        except Exception as ex:
            self._on_shutdown_complete(ex)

    cdef inline _on_shutdown_complete(self, shutdown_exc):
        if self._shutdown_timeout_handle is not None:
            self._shutdown_timeout_handle.cancel()
            self._shutdown_timeout_handle = None

        # we don't need the context here because TCP transport.close() should
        # be able to call connection_made() in the right context
        if shutdown_exc:
            self._fatal_error(shutdown_exc, 'Error occurred during shutdown')
        else:
            self._transport.close()

    cdef inline _abort(self, exc):
        self._set_state(UNWRAPPED)
        if self._transport is not None:
            self._transport._force_close(exc)

    # Outgoing flow

    cdef inline write(self, data):
        """Write some data bytes to the transport.

        This does not block; it buffers the data and arranges for it
        to be sent out asynchronously.
        """
        if not self._is_protocol_ready():
            return

        aiofn_validate_buffer(data)

        cdef:
            char * data_ptr
            Py_ssize_t data_len

        try:
            if self._write_backlog:
                if data:
                    self._write_backlog.append(aiofn_maybe_copy_buffer(data))
                return

            aiofn_unpack_buffer(data, &data_ptr, &data_len)
            if data_len == 0:
                return

            tail = self._write_impl(data, data_ptr, data_len, True)
            if tail is not None:
                self._write_backlog.append(tail)
                if self._is_debug:
                    _logger.debug("%r: appended %d bytes to write_backlog", self, len(tail))
                return
        except Exception as ex:
            self._fatal_error(ex, 'Fatal error on SSL protocol')

    cdef inline write_mem(self, char* data_ptr, Py_ssize_t data_len):
        if not self._is_protocol_ready():
            return

        if data_len == 0:
            return

        try:
            if self._write_backlog:
                self._write_backlog.append(PyBytes_FromStringAndSize(data_ptr, data_len))
                return

            tail = self._write_impl(None, data_ptr, data_len, True)
            if tail is not None:
                self._write_backlog.append(tail)
                return
        except Exception as ex:
            self._fatal_error(ex, 'Fatal error on SSL protocol')

    cdef inline writelines(self, list_of_data):
        """
        Write a list (or any iterable) of data bytes to the transport.
        """
        if not self._is_protocol_ready():
            return

        for data in list_of_data:
            aiofn_validate_buffer(data)

        cdef:
            char* data_ptr
            Py_ssize_t data_len
            bint add_to_backlog = False
            Py_ssize_t data_cnt = len(list_of_data)
            Py_ssize_t idx
            bint is_last

        try:
            if self._write_backlog:
                self._write_backlog.extend(aiofn_maybe_copy_buffer(data)
                                           for data in list_of_data if data)
                return

            for idx in range(data_cnt):
                data = list_of_data[idx]
                if add_to_backlog:
                    if len(data) > 0:
                        self._write_backlog.append(aiofn_maybe_copy_buffer(data))
                    continue

                aiofn_unpack_buffer(data, &data_ptr, &data_len)
                if data_len == 0:
                    continue

                is_last = idx == (data_cnt - 1)
                tail = self._write_impl(data, data_ptr, data_len, is_last)
                if tail is not None:
                    self._write_backlog.append(tail)
                    add_to_backlog = True
        except Exception as ex:
            self._fatal_error(ex, 'Fatal error on SSL protocol')

    cdef inline pause_reading(self):
        self._reading_paused = True
        self._transport.pause_reading()

    cdef inline resume_reading(self):
        if self._reading_paused:
            self._reading_paused = False
            self._loop.call_soon(self._do_read)
        self._transport.resume_reading()

    cpdef pause_writing(self):
        self._app_protocol.pause_writing()

    cpdef resume_writing(self):
        self._app_protocol.resume_writing()

    cdef inline bint _is_protocol_ready(self) except -1:
        if self._state in (FLUSHING, SHUTDOWN, UNWRAPPED):
            if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES:
                _logger.warning('SSL connection is closed')
            self._conn_lost += 1
            return False
        else:
            return True

    cdef inline _flush_write_backlog(self):
        if self._state not in (WRAPPED, FLUSHING):
            return

        cdef:
            Py_ssize_t backlog_size = len(self._write_backlog)
            char * data_ptr
            Py_ssize_t data_len
            bint add_to_backlog = False
            Py_ssize_t idx = 0
            Py_ssize_t items_completed = 0

        if backlog_size == 0:
            return

        try:
            for idx in range(len(self._write_backlog)):
                data = self._write_backlog[idx]
                aiofn_unpack_buffer(data, &data_ptr, &data_len)
                # Data was validated and cleared from empty objects in write/writelines

                tail = self._write_impl(data, data_ptr, data_len, True)
                if tail is not None:
                    self._write_backlog[idx] = tail
                    break
                else:
                    items_completed += 1
            del self._write_backlog[:items_completed]
        except Exception as ex:
            self._fatal_error(ex, 'Fatal error on SSL protocol')

    cdef inline _write_impl(self, data, char* data_ptr, Py_ssize_t data_len, bint is_last):
        """
        Do SSL_write, if outgoing BIO reach threshold size flush it to the 
        underlying protocol. If is_last=True, flush in the end regardless.
        If SSL_write returns SSL_ERROR_WANT_READ, materialize and return 
        remaining unsent part of the buffer. 
        On success return None.
        """
        cdef:
            size_t bytes_written
            int rc = 1
            int ssl_error

        while data_len != 0:
            # SSL_write_ex behave differently from non-blocking write syscall
            # If outgoing memory bio has some space, but doesn't have enough space
            # SSL_write_ex returns 0 and SSL_ERROR_WANT_WRITE is set.
            #
            # In such case we flush outgoing buffer and restart SSL_write_ex with
            # exactly same data_ptr, data_len.
            #
            # It is very confusing but bytes_written in such case may be > 16K
            # because SSL_write_ex, despite previously returning error,
            # has stored some of its processed data from the last step in its
            # own internal buffer.
            #
            # outgoing buffer in such case may receive 2 SSL records with one
            # SSL_write_ex call.
            rc = SSL_write_ex(self._ssl_connection.ssl_object, data_ptr, data_len, &bytes_written)

            if rc:
                if self._is_debug:
                    _logger.debug("%r: SSL_write_ex(..., %d, %d) = %d", self,
                                  data_len, bytes_written, rc)

                # Success path, we wrote all or some data
                if data_len == <Py_ssize_t>bytes_written:
                    self._maybe_send_outgoing(is_last)
                    return None

                # Not all data was written, this is most likely because outgoing
                # static BIO ran out of memory or full SSL record was written
                # (if SSL_ENABLE_PARTIAL_WRITE is set)
                data_ptr += bytes_written
                data_len -= bytes_written
                self._maybe_send_outgoing(False)
            else:
                ssl_error = SSL_get_error(self._ssl_connection.ssl_object, rc)

                if self._is_debug:
                    _logger.debug("%r: SSL_write_ex(..., %d, %d) = %d, ssl_error = %d",
                                  self, data_len, bytes_written, rc, ssl_error)

                # On any error we always need to flush outgoing BIO
                self._maybe_send_outgoing(True)

                # Since outgoing BIO is a static memory it may simply run out
                # of capacity
                if ssl_error == SSL_ERROR_WANT_WRITE:
                    continue

                # This is rare but still possible. SSL may refuse to send data
                # because of re-negotiation. Materialize and return remaining
                # data. We will proceed when new data arrives and re-negotiation
                # is complete
                if ssl_error == SSL_ERROR_WANT_READ:
                    return aiofn_maybe_copy_buffer_tail(data, data_ptr,
                                                        data_len)

                # Consider any other error as fatal, _write_impl caller will
                # initiate disconnect.
                raise self._ssl_connection.make_exc_from_ssl_error(
                    "SSL_write_ex failed", ssl_error)

    cdef inline _maybe_send_outgoing(self, bint is_last):
        # We call _maybe_send_outgoing if there MAY be some data to send,
        # Upstream logic doesn't check itself if there are actually some data
        # to send

        cdef:
            char* ptr
            long sz = BIO_get_mem_data(self._ssl_connection.outgoing, &ptr)

        if sz <= 0:
            return

        if sz < SSL_WRITE_BUFFER_SIZE and not is_last:
            # No need to send for now
            # _logger.info("Do not send now: outgoing_sz=%d, is_last=%d", sz, is_last)
            return

        self._transport.write_mem(ptr, sz)
        if self._is_debug:
            _logger.debug("Wrote %d bytes to TCP: is_last=%d", sz, is_last)

        if BIO_static_mem_consume(self._ssl_connection.outgoing, <size_t>sz) != 1:
            raise RuntimeError("BIO_static_mem_consume(outgoing) failed")

    # Incoming flow

    cpdef _do_read(self):
        if self._state not in (WRAPPED, FLUSHING):
            return
        try:
            if not self._reading_paused:
                if self._app_protocol_is_buffered:
                    self._do_read__buffered()
                else:
                    self._do_read__copied()

                self._maybe_send_outgoing(True)
                if self._write_backlog:
                    self._flush_write_backlog()
        except Exception as ex:
            self._fatal_error(ex, 'Fatal error on SSL protocol')

    cdef inline _do_read__buffered(self):
        if self._app_protocol_aiofn:
            app_buffer = (<Protocol>self._app_protocol).get_buffer(-1)
        else:
            app_buffer = self._app_protocol.get_buffer(-1)

        cdef:
            char* buf_ptr
            Py_ssize_t buf_len

        aiofn_unpack_buffer(app_buffer, &buf_ptr, &buf_len)

        if buf_len == 0:
            raise RuntimeError('get_buffer() returned an empty buffer')

        cdef:
            size_t last_bytes_read
            Py_ssize_t total_bytes_read = 0
            int rc = 0

        while buf_len > 0:
            rc = SSL_read_ex(
                self._ssl_connection.ssl_object,
                buf_ptr, buf_len, &last_bytes_read)

            if not rc:
                break
            buf_ptr += last_bytes_read
            buf_len -= last_bytes_read
            total_bytes_read += last_bytes_read
            if self._is_debug:
                _logger.debug("%r: SSL_read_ex(buf_len=%d, bytes_read=%d)=%d",
                              self, buf_len, last_bytes_read, rc)


        cdef int last_error = SSL_get_error(self._ssl_connection.ssl_object, rc)
        if self._is_debug:
            _logger.debug("%r: SSL_read_ex(buf_len=%d, bytes_read=%d)=%d, ssl_error=%d",
                          self, buf_len, last_bytes_read, rc, last_error)

        if total_bytes_read > 0:
            if self._app_protocol_aiofn:
                (<Protocol>self._app_protocol).buffer_updated(total_bytes_read)
            else:
                self._app_protocol.buffer_updated(total_bytes_read)

        # It could be that buffer_update user handler paused reading
        # But we do have extra data in the incoming BIO or in SSL object.
        # We could not read it because user provided buffer was too small.
        # Schedule _do_read immediately, and check in _do_read that reading is
        # not paused.
        # For resume_reading() check if we have some pending data for reading
        # and
        if buf_len == 0:
            self._loop.call_soon(self._do_read)
            return

        self._post_read(last_error)

    cdef inline _do_read__copied(self):
        cdef:
            size_t bytes_read
            list data = None
            PyObject* bytes_obj
            char* bytes_buffer_ptr
            bytes first_chunk = None, curr_chunk
            Py_ssize_t bytes_estimated
            int rc

        while True:
            bytes_estimated = (SSL_pending(self._ssl_connection.ssl_object) +
                               _bio_pending(self._ssl_connection.incoming)) + 256
            bytes_estimated = max(1024, bytes_estimated)

            bytes_obj = aiofn_allocate_bytes(bytes_estimated, &bytes_buffer_ptr)
            rc = SSL_read_ex(self._ssl_connection.ssl_object,
                             bytes_buffer_ptr,
                             bytes_estimated,
                             &bytes_read)

            if not rc:
                curr_chunk = aiofn_finalize_bytes(bytes_obj, 0)
                break
            else:
                curr_chunk = aiofn_finalize_bytes(bytes_obj, bytes_read)

            if self._is_debug:
                _logger.debug("%r: SSL_read_ex(buf_len=%d, bytes_read=%d)=%d",
                              self, bytes_estimated, bytes_read, rc)

            if first_chunk is None:
                first_chunk = curr_chunk
            elif data is None:
                data = [first_chunk, curr_chunk]
            else:
                data.append(curr_chunk)

        cdef int last_error = SSL_get_error(self._ssl_connection.ssl_object, rc)
        if self._is_debug:
            _logger.debug("%r: SSL_read_ex(buf_len=%d, bytes_read=%d)=%d, ssl_error=%d",
                          self, bytes_estimated, bytes_read, rc, last_error)

        user_data = None
        if data is not None:
            user_data = b''.join(data)
        elif first_chunk is not None:
            user_data = first_chunk

        if user_data is not None:
            if self._app_protocol_aiofn:
                (<Protocol>self._app_protocol).data_received(user_data)
            else:
                self._app_protocol.data_received(user_data)

        self._post_read(last_error)

    cdef inline _post_read(self, int last_error):
        if last_error in (SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE):
            return

        if last_error == SSL_ERROR_ZERO_RETURN and SSL_get_shutdown(self._ssl_connection.ssl_object) == SSL_RECEIVED_SHUTDOWN:
            # close_notify
            self._call_eof_received()
            self._start_shutdown()
            return

        raise self._ssl_connection.make_exc_from_ssl_error("SSL_read_ex failed", last_error)

    cdef inline _call_eof_received(self):
        if self._app_state == STATE_CON_MADE:
            self._app_state = STATE_EOF
            try:
                keep_open = self._app_protocol.eof_received()
            except (KeyboardInterrupt, SystemExit):
                raise
            except BaseException as ex:
                self._fatal_error(ex, 'Error calling eof_received()')
            else:
                if keep_open:
                    _logger.warning('returning true from eof_received() '
                                       'has no effect when using ssl')

    cdef inline _fatal_error(self, exc, message='Fatal error on transport'):
        self._abort(exc)

        if isinstance(exc, OSError):
            if self._loop.get_debug():
                _logger.debug("%r: %s", self, message, exc_info=True)
        elif not isinstance(exc, asyncio.CancelledError):
            self._loop.call_exception_handler({
                'message': message,
                'exception': exc,
                'transport': self._transport,
                'protocol': self,
            })

    cdef inline _fatal_error_no_close(self, exc, message='Fatal error on transport'):
        if isinstance(exc, OSError):
            if self._loop.get_debug():
                _logger.debug("%r: %s", self, message, exc_info=True)
        else:
            self._loop.call_exception_handler({
                'message': message,
                'exception': exc,
                'transport': self._transport,
                'protocol': self,
            })

    # Used for testing only
    def _allow_renegotiation(self):
        SSL_set_options(
            self._ssl_connection.ssl_object,
            SSL_OP_ALLOW_CLIENT_RENEGOTIATION |
            SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
        )

    # Used for testing only
    def _renegotiate(self):
        if self._state != WRAPPED:
            raise RuntimeError(
                "renegotiation requires an active wrapped SSL connection")

        cipher = self._extra.get("cipher")
        if cipher is not None and len(cipher) >= 2 and cipher[1] == "TLSv1.3":
            raise NotImplementedError(
                "TLS 1.3 does not support classic SSL renegotiation")

        cdef:
            int rc
            int ssl_error
            SSL * ssl_object = self._ssl_connection.ssl_object

        try:
            rc = SSL_renegotiate(ssl_object)
            if rc != 1:
                raise RuntimeError(f"ssl renegotiation request failed")

            # self._set_state(DO_HANDSHAKE)
            self._do_handshake()
        except Exception as ex:
            self._fatal_error(ex, "Fatal error on SSL renegotiation")
