#!/usr/bin/env python
# coding=utf8
# Copyright 2015 Syfun
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import errno
import os
import time
import socket
import ssl

import greenlet
import eventlet
from eventlet import wsgi
from webob import dec, exc, Response as Resp, Request as Req

from pweby import exception
from pweby import log as logging
from pweby.config import set_config
from pweby.utils import save_and_reraise_exception, set_tcp_keepalive
from pweby.mapper import Mapper


LOG = logging.getLogger(__name__)

config = {
    # Sets the value of TCP_KEEPALIVE (True/False) for each
    # server socket. Default is True.
    'tcp_keepalive': True,
    # Sets the value of TCP_KEEPIDLE in seconds for each server
    # socket. Not supported on OS X. Default is 600.
    'tcp_keepidle': 600,
    # Sets the value of TCP_KEEPINTVL in seconds for each server
    # socket. Not supported on OS X.
    'tcp_keepalive_interval': None,
    # Sets the value of TCP_KEEPCNT for each server socket. Not
    # supported on OS X.
    'tcp_keepalive_count': None,
    # CA certificate file to use to verify connecting clients.
    # Default is None.
    'ssl_ca_file': None,
    # Certificate file to use when starting the server securely.
    # Default is None.
    'ssl_cert_file': None,
    # Private key file to use when starting the server securely.
    # Default is None.
    'ssl_key_file': None,
    # Maximum line size of message headers to be accepted.
    # max_header_line may need to be increased when using
    # large tokens (typically those generated by the Keystone v3 API
    # with big service catalogs). Default is 16384.
    'max_header_line': 16384,
    # Timeout for client connections\' socket operations. If an incoming
    # connection is idle for this number of seconds it will be closed.
    # A value of \'0\' means wait forever. Default is 900.
    'client_socket_timeout': 900,
    # If False, closes the client socket connection explicitly.
    # Setting it to True to maintain backward compatibility.
    # Recommended setting is set it to False. Default is True.
    'wsgi_keep_alive': True,
}
CONF = set_config(config_json=config)


class Server(object):
    """Server class to manage a WSGI server, serving a WSGI application."""

    default_pool_size = 1000

    def __init__(self, app, name='pweby server', host=None, port=None, pool_size=None,
                 protocol=eventlet.wsgi.HttpProtocol, backlog=128):
        """Initialize, but do not start, a WSGI server.

        :param name: Pretty name for logging.
        :param app: The WSGI application to serve.
        :param host: IP address to serve the application.
        :param port: Port number to server the application.
        :param pool_size: Maximum number of eventlets to spawn concurrently.
        :returns: None

        """
        # Allow operators to customize http requests max header line size.
        eventlet.wsgi.MAX_HEADER_LINE = CONF.max_header_line
        self.client_socket_timeout = CONF.client_socket_timeout or None
        self.app = self._set_app(app)
        self.name = name
        self._host = host or "0.0.0.0"
        self._port = port or 8090
        self._server = None
        self._socket = None
        self._protocol = protocol
        self.pool_size = pool_size or self.default_pool_size
        self._pool = eventlet.GreenPool(self.pool_size)
        self._logger = logging.getLogger("eventlet.wsgi.server")
        self._wsgi_logger = logging.WritableLogger(self._logger)

        if backlog < 1:
            raise exception.InvalidInput(
                reason='The backlog must be more than 1')

        bind_addr = (self._host, self._port)
        # TODO(dims): eventlet's green dns/socket module does not actually
        # support IPv6 in getaddrinfo(). We need to get around this in the
        # future or monitor upstream for a fix
        try:
            info = socket.getaddrinfo(bind_addr[0],
                                      bind_addr[1],
                                      socket.AF_UNSPEC,
                                      socket.SOCK_STREAM)[0]
            family = info[0]
            bind_addr = info[-1]
        except Exception:
            family = socket.AF_INET

        cert_file = CONF.ssl_cert_file
        key_file = CONF.ssl_key_file
        ca_file = CONF.ssl_ca_file
        self._use_ssl = cert_file or key_file

        if cert_file and not os.path.exists(cert_file):
            raise RuntimeError("Unable to find cert_file : %s" % cert_file)

        if ca_file and not os.path.exists(ca_file):
            raise RuntimeError("Unable to find ca_file : %s" % ca_file)

        if key_file and not os.path.exists(key_file):
            raise RuntimeError("Unable to find key_file : %s" % key_file)

        if self._use_ssl and (not cert_file or not key_file):
            raise RuntimeError("When running server in SSL mode, you "
                                 "must specify both a cert_file and "
                                 "key_file option value in your "
                                 "configuration file.")

        retry_until = time.time() + 30
        while not self._socket and time.time() < retry_until:
            try:
                self._socket = eventlet.listen(bind_addr, backlog=backlog,
                                               family=family)
            except socket.error as err:
                if err.args[0] != errno.EADDRINUSE:
                    raise
                eventlet.sleep(0.1)

        if not self._socket:
            raise RuntimeError("Could not bind to %(host)s:%(port)s "
                               "after trying for 30 seconds" %
                               {'host': host, 'port': port})

        (self._host, self._port) = self._socket.getsockname()[0:2]
        LOG.info("%(name)s listening on %(_host)s:%(_port)s" %
                 {'name': self.name, '_host': self._host, '_port': self._port})

    def _set_app(self, app):
        try:
            if is_app(app):
                return MainHandler(app)
        except TypeError:
            pass
        try:
            if isinstance(app, MainHandler):
                return app
        except TypeError:
            raise exception.InvalidApplication(app=app.__name__)

    def start(self):
        """Start serving a WSGI application.

        :returns: None
        :raises: cinder.exception.InvalidInput

        """
        # The server socket object will be closed after server exits,
        # but the underlying file descriptor will remain open, and will
        # give bad file descriptor error. So duplicating the socket object,
        # to keep file descriptor usable.

        dup_socket = self._socket.dup()
        dup_socket.setsockopt(socket.SOL_SOCKET,
                              socket.SO_REUSEADDR, 1)

        # Call set_tcp_keepalive in oslo to set
        # tcp keepalive parameters. Sockets can hang around forever
        # without keepalive
        set_tcp_keepalive(dup_socket,
                          CONF.tcp_keepalive,
                          CONF.tcp_keepidle,
                          CONF.tcp_keepalive_count,
                          CONF.tcp_keepalive_interval)

        if self._use_ssl:
            try:
                ssl_kwargs = {
                    'server_side': True,
                    'certfile': CONF.ssl_cert_file,
                    'keyfile': CONF.ssl_key_file,
                    'cert_reqs': ssl.CERT_NONE,
                }

                if CONF.ssl_ca_file:
                    ssl_kwargs['ca_certs'] = CONF.ssl_ca_file
                    ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED

                dup_socket = ssl.wrap_socket(dup_socket,
                                             **ssl_kwargs)
            except Exception:
                with save_and_reraise_exception():
                    LOG.error("Failed to start %(name)s on %(_host)s:"
                                  "%(_port)s with SSL "
                                  "support." % self.__dict__)

        wsgi_kwargs = {
            'func': eventlet.wsgi.server,
            'sock': dup_socket,
            'site': self.app,
            'protocol': self._protocol,
            'custom_pool': self._pool,
            'log': self._wsgi_logger,
            'socket_timeout': self.client_socket_timeout,
            'keepalive': CONF.wsgi_keep_alive
        }

        self._server = eventlet.spawn(**wsgi_kwargs)

    @property
    def host(self):
        return self._host

    @property
    def port(self):
        return self._port

    def serve(self):
        self.start()
        self.wait()

    def stop(self):
        """Stop this server.

        This is not a very nice action, as currently the method by which a
        server is stopped is by killing its eventlet.

        :returns: None

        """
        LOG.info("Stopping WSGI server.")
        if self._server is not None:
            # Resize pool to stop new requests from being processed
            self._pool.resize(0)
            self._server.kill()

    def wait(self):
        """Block, until the server has stopped.

        Waits on the server's eventlet to finish, then returns.

        :returns: None

        """
        try:
            if self._server is not None:
                self._pool.waitall()
                self._server.wait()
        except greenlet.GreenletExit:
            LOG.info("WSGI server has stopped.")

    def reset(self):
        """Reset server greenpool size to default.

        :returns: None

        """
        self._pool.resize(self.pool_size)


class Request(Req):
    pass


class Response(Resp):
    pass


class Application(object):
    """
    Base WSGI application wrapper.
    Subclasses need to implement process_request.
     """
    filters = []
    prefix = ''
    prefix_more = {}
    methods = []

    def __new__(cls, handler):
        app = super(Application, cls).__new__(cls)
        app.handler = handler
        _app = add_filters(app, cls.filters)
        for _, func in cls.__dict__.items():
            try:
                _url = getattr(func, '_url')
                _kwargs = getattr(func, '_kwargs')
            except AttributeError:
                continue
            else:
                # func's priority of request method is higher.
                _kwargs['requirements'].update(app.prefix_more)
                _kwargs['conditions']['method'] = app.methods
                controller = '%s&&&%s' % (str(_app), func.__name__)
                app.handler.mapper.connect(
                    app.prefix + _url,
                    controller=controller,
                    **_kwargs)

        return _app

    @dec.wsgify
    def __call__(self, req):
        func_name = req.environ['_func_name']
        kwargs = req.environ['_kwargs']
        func = getattr(self, func_name)
        return func(req, **kwargs)


class Filter(object):

    @classmethod
    def factory(cls, application):
        return cls(application)

    def __init__(self, application):
        self.application = application

    @dec.wsgify
    def __call__(self, req):
        response = self.process_request(req)
        if response:
            return response
        response = req.get_response(self.application)
        return self.process_response(response)

    def process_request(self, req):
        """Called on each request.

        If this returns None, the next application down the stack will be
        executed. If it returns a response then that response will be returned
        and execution will stop here.

        """
        return None

    def process_response(self, response):
        """Do whatever you'd like to the response."""
        return response


def is_app(app):
    """

    :param app:
    :return:
    """
    try:
        if issubclass(app, Application):
            return True
    except TypeError:
        return False


def is_filter(filter):
    """

    :param filter:
    :return:
    """
    try:
        if issubclass(filter, Filter):
            return True
    except TypeError:
        return False


def add_filters(app, filters):
    """

    :param app:
    :param filters:
    :return:
    """
    # _app = deepcopy(app)
    if filters:
        filters.reverse()
        for f in filters:
            if not is_filter(f):
                LOG.error('not a middleware')
                raise Exception()
            app = f.factory(app)
    return app


class MainHandler(object):
    """

    """
    def __init__(self, *apps):
        self.mapper = Mapper()
        self.apps = {}
        for app in apps:
            self.add_app(app)

    @dec.wsgify
    def __call__(self, req):
        result = self.mapper.match(req.path)
        if not result:
            return exc.HTTPNotFound()
        app_name, func_name = result.pop('controller').split('&&&')
        app = self.apps[app_name]
        req.environ['_func_name'] = func_name
        req.environ['_kwargs'] = result
        return req.get_response(app)

    def add_app(self, app):
        """

        :param app:
        :return:
        """
        if not is_app(app):
            raise exception.InvalidApplication(app=app.__name__)
        _app = app(self)
        self.apps[str(_app)] = _app
