import os.path
import json
import datetime
import logging

import twisted.internet.endpoints
import twisted.internet.threads
import twisted.web.wsgi
import twisted.web.static
import twisted.web.resource
import twisted.web.server
# import twisted.web.util
import twisted.logger
import twisted.python.logfile

import mini_buildd.config
import mini_buildd.threads

LOG = logging.getLogger(__name__)


class Site(twisted.web.server.Site):
    def _openLogFile(self, path):  # noqa (pep8 N802)
        return twisted.python.logfile.LogFile(os.path.basename(path), directory=os.path.dirname(path), rotateLength=5000000, maxRotatedFiles=9)


class RootResource(twisted.web.resource.Resource):
    """Twisted root resource needed to mix static and wsgi resources."""

    def __init__(self, wsgi_resource):
        super().__init__()
        self._wsgi_resource = wsgi_resource

    def getChild(self, path, request):  # noqa (pep8 N802)
        request.prepath.pop()
        request.postpath.insert(0, path)
        return self._wsgi_resource


class FileResource(twisted.web.static.File):
    """Twisted static resource enhanced with switchable index and regex matching support."""

    DOC_MISSING_HTML = ("<html><body>"
                        "<h1>Online Manual Not Available (<code>mini-buildd-doc</code> not installed?)</h1>"
                        "Package <b><code>mini-buildd-doc</code></b> is not installed on this site (might be intentional to save space)."
                        "<p><a href='/'>[Back]</a></p>"
                        "</body></html>")

    def __init__(self, *args, mbd_uri=None, mbd_path=None, **kwargs):
        self.mbd_uri = mbd_uri
        self.mbd_path = mbd_path

        if self.mbd_path is not None:
            super().__init__(mbd_path.full, *args, **kwargs, defaultType=f"text/plain; charset={mini_buildd.config.CHAR_ENCODING}")
        else:
            super().__init__(*args, **kwargs)

        if self.mbd_uri is not None and mbd_uri.with_doc_missing_error:
            self.childNotFound = twisted.web.resource.NoResource(self.DOC_MISSING_HTML)

    def directoryListing(self):  # noqa (pep8 N802)
        if not self.mbd_uri.with_index:
            return self.forbidden
        return twisted.web.static.DirectoryLister(self.path, self.listNames(), self.contentTypes, self.contentEncodings, self.defaultType)

    def getChild(self, path, request):  # noqa (pep8 N802)
        """Custom getChild implementation."""
        if not self.mbd_uri.auth.is_authorized(None):
            # When there is (?) proper auth support, correct thing would be to redirect to login..
            # uri = f"{mini_buildd.config.URIS['accounts']['login']}?next={request.uri.decode(mini_buildd.config.CHAR_ENCODING)}"
            # return twisted.web.util.Redirect(bytes(uri, encoding=mini_buildd.config.CHAR_ENCODING))
            # ..but for now:
            return twisted.web.resource.ForbiddenResource(message="Static URIs do not have authorization support (yet). Please use django view URI for now.")

        regex = self.mbd_uri.regex
        if regex is not None and not regex.match(request.uri.decode(mini_buildd.config.CHAR_ENCODING)):
            return twisted.web.resource.ForbiddenResource(message="Path does not match allowed regex")

        child = super().getChild(path, request)
        child.mbd_uri = self.mbd_uri  # getChild() implicitly calls our __init__ again. There seem to be no way to get around this monkey patch.
        child.mbd_path = self.mbd_path  # getChild() implicitly calls our __init__ again. There seem to be no way to get around this monkey patch.
        return child

    def render(self, request):
        if self.mbd_uri.cache_ttl:
            request.setHeader("Cache-Control", f"public, max-age={self.mbd_uri.cache_ttl}, no-transform")
        return super().render(request)


class Events(twisted.web.resource.Resource):
    @classmethod
    def _render(cls, request):
        try:
            LOG.debug(f"{request.channel}: Waiting for event...")
            since = datetime.datetime.fromisoformat(request.args[b"since"][0].decode(mini_buildd.config.CHAR_ENCODING)) if b"since" in request.args else None
            queue = mini_buildd.get_daemon().events_queue.attach(request.channel, since=mini_buildd.misc.Datetime.check_aware(since))
            event = queue.get()
            LOG.debug(f"{request.channel}: Got event: {event}")
            if event is mini_buildd.config.SHUTDOWN:
                raise mini_buildd.HTTPShutdown
            request.write(json.dumps(event.to_json()).encode(mini_buildd.config.CHAR_ENCODING))
            request.finish()
        except Exception as e:
            http_exception = mini_buildd.e2http(e)
            request.setResponseCode(500, f"{http_exception}".encode(mini_buildd.config.CHAR_ENCODING))
            if getattr(request, "_disconnected", False):
                request.notifyFinish()
            else:
                request.finish()

    @classmethod
    def mbd_ssl_workaround(cls, request):
        """
        Twisted SSL timeout workaround (avoids spurious disconnects w/ SSL).

        WTF: When using SSL, (event queue) connections spuriously disconnect (twisted: "Forcibly timing out client"), even
        though no timeout has been specified (timeOut=None). Manually disabling 'abortTimeout' as well (which otherwise
        defaults to 15 seconds) seems to fix the SSL case (and hopefully has no other bad effects).
        """
        what = cls.mbd_ssl_workaround.__doc__.splitlines()[1].strip()
        try:
            request.channel.abortTimeout = None
            LOG.warning(f"{request.channel}: {what}: Disabled 'abortTimeout' (timeOut={request.channel.timeOut}, abortTimeout={request.channel.abortTimeout}).")
        except BaseException as e:
            mini_buildd.log_exception(LOG, what, e)

    def render_GET(self, request):  # pylint: disable=invalid-name  # noqa (pep8 N802)
        self.mbd_ssl_workaround(request)
        request.setHeader("Content-Type", f"application/json; charset={mini_buildd.config.CHAR_ENCODING}")
        twisted.internet.threads.deferToThread(self._render, request)
        return twisted.web.server.NOT_DONE_YET


class HttpD(mini_buildd.threads.Thread):
    def _add_route(self, uri, resource):
        """Add route from (possibly nested) path from config -- making sure already existing parent routes are re-used."""
        LOG.info(f"Adding route: {uri} -> {resource}")

        uri_split = uri.twisted().split("/")
        res = self.resource
        for u in [uri_split[0:p] for p in range(1, len(uri_split))]:
            path = "/".join(u)
            sub = self.route_hierarchy.get(path)
            if sub is None:
                sub = twisted.web.resource.Resource()
                res.putChild(bytes(u[-1], encoding=mini_buildd.config.CHAR_ENCODING), sub)
                self.route_hierarchy[path] = sub
            res = sub
        res.putChild(bytes(uri_split[-1], encoding=mini_buildd.config.CHAR_ENCODING), resource)

    def __init__(self, wsgi_app):
        super().__init__(name="httpd")
        self.route_hierarchy = {}

        # Bend twisted (not access.log) logging to ours
        twisted.logger.globalLogPublisher.addObserver(twisted.logger.STDLibLogObserver(name=__name__))

        # HTTP setup
        self.resource = RootResource(twisted.web.wsgi.WSGIResource(mini_buildd.net.reactor(), mini_buildd.net.reactor().getThreadPool(), wsgi_app))  # pylint: disable=no-member
        self.site = Site(self.resource, logPath=mini_buildd.config.ROUTES["log"].path.join(mini_buildd.config.ACCESS_LOG_FILE))

        # Static routes
        for route in mini_buildd.config.ROUTES.values():
            for uri in (uri for key, uri in route.uris.items() if key.startswith("static")):
                self._add_route(uri, FileResource(mbd_uri=uri, mbd_path=route.path))

        # Events route
        self._add_route(mini_buildd.config.ROUTES["events"].uris["attach"], Events())

        # Start sockets
        for ep in mini_buildd.config.HTTP_ENDPOINTS:
            twisted.internet.endpoints.serverFromString(mini_buildd.net.reactor(), mini_buildd.net.ServerEndpoint(ep, protocol="http").description).listen(self.site)

    def shutdown(self):
        mini_buildd.net.reactor().stop()  # pylint: disable=no-member

    def run(self):
        mini_buildd.net.reactor().run(installSignalHandlers=0)  # pylint: disable=no-member
        LOG.debug(f"{self}: FINISHED")
