##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################

import os
import os.path
import socket
import time

from waitress import trigger
from waitress.adjustments import Adjustments
from waitress.channel import HTTPChannel
from waitress.task import ThreadedTaskDispatcher
from waitress.utilities import cleanup_unix_socket

from waitress.compat import (
    IPPROTO_IPV6,
    IPV6_V6ONLY,
)
from . import wasyncore
from .proxy_headers import proxy_headers_middleware


def create_server(
    application,
    map=None,
    _start=True,  # test shim
    _sock=None,  # test shim
    _dispatcher=None,  # test shim
    **kw  # adjustments
):
    """
    if __name__ == '__main__':
        server = create_server(app)
        server.run()
    """
    if application is None:
        raise ValueError(
            'The "app" passed to ``create_server`` was ``None``.  You forgot '
            "to return a WSGI app within your application."
        )
    adj = Adjustments(**kw)

    if map is None:  # pragma: nocover
        map = {}

    dispatcher = _dispatcher
    if dispatcher is None:
        dispatcher = ThreadedTaskDispatcher()
        dispatcher.set_thread_count(adj.threads)

    if adj.unix_socket and hasattr(socket, "AF_UNIX"):
        sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None)
        return UnixWSGIServer(
            application,
            map,
            _start,
            _sock,
            dispatcher=dispatcher,
            adj=adj,
            sockinfo=sockinfo,
        )

    effective_listen = []
    last_serv = None
    if not adj.sockets:
        for sockinfo in adj.listen:
            # When TcpWSGIServer is called, it registers itself in the map. This
            # side-effect is all we need it for, so we don't store a reference to
            # or return it to the user.
            last_serv = TcpWSGIServer(
                application,
                map,
                _start,
                _sock,
                dispatcher=dispatcher,
                adj=adj,
                sockinfo=sockinfo,
            )
            effective_listen.append(
                (last_serv.effective_host, last_serv.effective_port)
            )

    for sock in adj.sockets:
        sockinfo = (sock.family, sock.type, sock.proto, sock.getsockname())
        if sock.family == socket.AF_INET or sock.family == socket.AF_INET6:
            last_serv = TcpWSGIServer(
                application,
                map,
                _start,
                sock,
                dispatcher=dispatcher,
                adj=adj,
                bind_socket=False,
                sockinfo=sockinfo,
            )
            effective_listen.append(
                (last_serv.effective_host, last_serv.effective_port)
            )
        elif hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX:
            last_serv = UnixWSGIServer(
                application,
                map,
                _start,
                sock,
                dispatcher=dispatcher,
                adj=adj,
                bind_socket=False,
                sockinfo=sockinfo,
            )
            effective_listen.append(
                (last_serv.effective_host, last_serv.effective_port)
            )

    # We are running a single server, so we can just return the last server,
    # saves us from having to create one more object
    if len(effective_listen) == 1:
        # In this case we have no need to use a MultiSocketServer
        return last_serv

    # Return a class that has a utility function to print out the sockets it's
    # listening on, and has a .run() function. All of the TcpWSGIServers
    # registered themselves in the map above.
    return MultiSocketServer(map, adj, effective_listen, dispatcher)


# This class is only ever used if we have multiple listen sockets. It allows
# the serve() API to call .run() which starts the wasyncore loop, and catches
# SystemExit/KeyboardInterrupt so that it can atempt to cleanly shut down.
class MultiSocketServer(object):
    asyncore = wasyncore  # test shim

    def __init__(
        self, map=None, adj=None, effective_listen=None, dispatcher=None,
    ):
        self.adj = adj
        self.map = map
        self.effective_listen = effective_listen
        self.task_dispatcher = dispatcher

    def print_listen(self, format_str):  # pragma: nocover
        for l in self.effective_listen:
            l = list(l)

            if ":" in l[0]:
                l[0] = "[{}]".format(l[0])

            print(format_str.format(*l))

    def run(self):
        try:
            self.asyncore.loop(
                timeout=self.adj.asyncore_loop_timeout,
                map=self.map,
                use_poll=self.adj.asyncore_use_poll,
            )
        except (SystemExit, KeyboardInterrupt):
            self.close()

    def close(self):
        self.task_dispatcher.shutdown()
        wasyncore.close_all(self.map)


class BaseWSGIServer(wasyncore.dispatcher, object):

    channel_class = HTTPChannel
    next_channel_cleanup = 0
    socketmod = socket  # test shim
    asyncore = wasyncore  # test shim

    def __init__(
        self,
        application,
        map=None,
        _start=True,  # test shim
        _sock=None,  # test shim
        dispatcher=None,  # dispatcher
        adj=None,  # adjustments
        sockinfo=None,  # opaque object
        bind_socket=True,
        **kw
    ):
        if adj is None:
            adj = Adjustments(**kw)

        if adj.trusted_proxy or adj.clear_untrusted_proxy_headers:
            # wrap the application to deal with proxy headers
            # we wrap it here because webtest subclasses the TcpWSGIServer
            # directly and thus doesn't run any code that's in create_server
            application = proxy_headers_middleware(
                application,
                trusted_proxy=adj.trusted_proxy,
                trusted_proxy_count=adj.trusted_proxy_count,
                trusted_proxy_headers=adj.trusted_proxy_headers,
                clear_untrusted=adj.clear_untrusted_proxy_headers,
                log_untrusted=adj.log_untrusted_proxy_headers,
                logger=self.logger,
            )

        if map is None:
            # use a nonglobal socket map by default to hopefully prevent
            # conflicts with apps and libs that use the wasyncore global socket
            # map ala https://github.com/Pylons/waitress/issues/63
            map = {}
        if sockinfo is None:
            sockinfo = adj.listen[0]

        self.sockinfo = sockinfo
        self.family = sockinfo[0]
        self.socktype = sockinfo[1]
        self.application = application
        self.adj = adj
        self.trigger = trigger.trigger(map)
        if dispatcher is None:
            dispatcher = ThreadedTaskDispatcher()
            dispatcher.set_thread_count(self.adj.threads)

        self.task_dispatcher = dispatcher
        self.asyncore.dispatcher.__init__(self, _sock, map=map)
        if _sock is None:
            self.create_socket(self.family, self.socktype)
            if self.family == socket.AF_INET6:  # pragma: nocover
                self.socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1)

        self.set_reuse_addr()

        if bind_socket:
            self.bind_server_socket()

        self.effective_host, self.effective_port = self.getsockname()
        self.server_name = self.get_server_name(self.effective_host)
        self.active_channels = {}
        if _start:
            self.accept_connections()

    def bind_server_socket(self):
        raise NotImplementedError  # pragma: no cover

    def get_server_name(self, ip):
        """Given an IP or hostname, try to determine the server name."""

        if not ip:
            raise ValueError("Requires an IP to get the server name")

        server_name = str(ip)

        # If we are bound to all IP's, just return the current hostname, only
        # fall-back to "localhost" if we fail to get the hostname
        if server_name == "0.0.0.0" or server_name == "::":
            try:
                return str(self.socketmod.gethostname())
            except (socket.error, UnicodeDecodeError):  # pragma: no cover
                # We also deal with UnicodeDecodeError in case of Windows with
                # non-ascii hostname
                return "localhost"

        # Now let's try and convert the IP address to a proper hostname
        try:
            server_name = self.socketmod.gethostbyaddr(server_name)[0]
        except (socket.error, UnicodeDecodeError):  # pragma: no cover
            # We also deal with UnicodeDecodeError in case of Windows with
            # non-ascii hostname
            pass

        # If it contains an IPv6 literal, make sure to surround it with
        # brackets
        if ":" in server_name and "[" not in server_name:
            server_name = "[{}]".format(server_name)

        return server_name

    def getsockname(self):
        raise NotImplementedError  # pragma: no cover

    def accept_connections(self):
        self.accepting = True
        self.socket.listen(self.adj.backlog)  # Get around asyncore NT limit

    def add_task(self, task):
        self.task_dispatcher.add_task(task)

    def readable(self):
        now = time.time()
        if now >= self.next_channel_cleanup:
            self.next_channel_cleanup = now + self.adj.cleanup_interval
            self.maintenance(now)
        return self.accepting and len(self._map) < self.adj.connection_limit

    def writable(self):
        return False

    def handle_read(self):
        pass

    def handle_connect(self):
        pass

    def handle_accept(self):
        try:
            v = self.accept()
            if v is None:
                return
            conn, addr = v
        except socket.error:
            # Linux: On rare occasions we get a bogus socket back from
            # accept.  socketmodule.c:makesockaddr complains that the
            # address family is unknown.  We don't want the whole server
            # to shut down because of this.
            if self.adj.log_socket_errors:
                self.logger.warning("server accept() threw an exception", exc_info=True)
            return
        self.set_socket_options(conn)
        addr = self.fix_addr(addr)
        self.channel_class(self, conn, addr, self.adj, map=self._map)

    def run(self):
        try:
            self.asyncore.loop(
                timeout=self.adj.asyncore_loop_timeout,
                map=self._map,
                use_poll=self.adj.asyncore_use_poll,
            )
        except (SystemExit, KeyboardInterrupt):
            self.task_dispatcher.shutdown()

    def pull_trigger(self):
        self.trigger.pull_trigger()

    def set_socket_options(self, conn):
        pass

    def fix_addr(self, addr):
        return addr

    def maintenance(self, now):
        """
        Closes channels that have not had any activity in a while.

        The timeout is configured through adj.channel_timeout (seconds).
        """
        cutoff = now - self.adj.channel_timeout
        for channel in self.active_channels.values():
            if (not channel.requests) and channel.last_activity < cutoff:
                channel.will_close = True

    def print_listen(self, format_str):  # pragma: nocover
        print(format_str.format(self.effective_host, self.effective_port))

    def close(self):
        self.trigger.close()
        return wasyncore.dispatcher.close(self)


class TcpWSGIServer(BaseWSGIServer):
    def bind_server_socket(self):
        (_, _, _, sockaddr) = self.sockinfo
        self.bind(sockaddr)

    def getsockname(self):
        try:
            return self.socketmod.getnameinfo(
                self.socket.getsockname(), self.socketmod.NI_NUMERICSERV
            )
        except:  # pragma: no cover
            # This only happens on Linux because a DNS issue is considered a
            # temporary failure that will raise (even when NI_NAMEREQD is not
            # set). Instead we try again, but this time we just ask for the
            # numerichost and the numericserv (port) and return those. It is
            # better than nothing.
            return self.socketmod.getnameinfo(
                self.socket.getsockname(),
                self.socketmod.NI_NUMERICHOST | self.socketmod.NI_NUMERICSERV,
            )

    def set_socket_options(self, conn):
        for (level, optname, value) in self.adj.socket_options:
            conn.setsockopt(level, optname, value)


if hasattr(socket, "AF_UNIX"):

    class UnixWSGIServer(BaseWSGIServer):
        def __init__(
            self,
            application,
            map=None,
            _start=True,  # test shim
            _sock=None,  # test shim
            dispatcher=None,  # dispatcher
            adj=None,  # adjustments
            sockinfo=None,  # opaque object
            **kw
        ):
            if sockinfo is None:
                sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None)

            super(UnixWSGIServer, self).__init__(
                application,
                map=map,
                _start=_start,
                _sock=_sock,
                dispatcher=dispatcher,
                adj=adj,
                sockinfo=sockinfo,
                **kw
            )

        def bind_server_socket(self):
            cleanup_unix_socket(self.adj.unix_socket)
            self.bind(self.adj.unix_socket)
            if os.path.exists(self.adj.unix_socket):
                os.chmod(self.adj.unix_socket, self.adj.unix_socket_perms)

        def getsockname(self):
            return ("unix", self.socket.getsockname())

        def fix_addr(self, addr):
            return ("localhost", None)

        def get_server_name(self, ip):
            return "localhost"


# Compatibility alias.
WSGIServer = TcpWSGIServer
