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.logger
import twisted.python.logfile
import twisted.python.threadpool
from twisted.internet import reactor

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


def mbd_is_ssl(request):
    try:
        return request.transport.__class__.__name__ == "TLSMemoryBIOProtocol"
    except Exception as e:
        LOG.warning(f"Hack to determine SSL failed: {e}")
        return False


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

    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)

    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

    @classmethod
    def mbd_producer_workaround(cls, request):
        """
        .. attention:: **compat** (``twisted``): Always unregister producer (avoids random static file error)

        Avoids seemingly random::

          RuntimeError: Cannot register producer <twisted.web.static.NoRangeStaticProducer object at 0xfoo>, because producer <twisted.internet._producer_helpers._PullToPush object at 0xbar> was never unregistered.

        errors from twisted. This in turn randomly breaks ``apt update`` calls with (slightly misleading) 'size mismatch' errors.

        Rare enough to never ever have shown in testsuite runs, but on bigger repos (with many (parallel?) apt calls), it's almost certain to appear.
        """
        what = cls.mbd_producer_workaround.__doc__.splitlines()[1].strip()
        try:
            # request.channel.unregisterProducer()
            request.channel.loseConnection()
        except BaseException as e:
            mini_buildd.log_exception(LOG, what, e)

    def render(self, request):
        if not mbd_is_ssl(request):
            self.mbd_producer_workaround(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):
        LOG.debug(f"{request.channel}: Waiting for event...")
        after = datetime.datetime.fromisoformat(request.args[b"after"][0].decode(mini_buildd.config.CHAR_ENCODING)) if b"after" in request.args else None
        queue = mini_buildd.get_daemon().events.attach(request.channel, after=mini_buildd.misc.Datetime.check_aware(after))
        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()

    @classmethod
    def _on_error(cls, failure, request):
        http_exception = mini_buildd.e2http(failure.value)
        request.setResponseCode(http_exception.status, 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):
        """
        .. attention:: **compat** (``twisted < 22.2``): 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).

          Retests with ``twisted 22.2`` could not (yet) reproduce this behaviour. So I guess it would be fine
          to remove this once we can pimp the dependency in ``debian/control``.

          Retests with ``twisted 22.4`` occasionally also showed (twisted: "Timing out client"), so I added
          ``setTimeout(None)`` as well. I.e., I had no such failures with that workaround, however I also had some
          *successful* testsuite runs w/o that workaround.
        """
        what = cls.mbd_ssl_workaround.__doc__.splitlines()[1].strip()
        try:
            LOG.debug(f"{request.channel}: PRE: timeOut={request.channel.timeOut}, abortTimeout={request.channel.abortTimeout}")
            request.channel.abortTimeout = None
            request.channel.setTimeout(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):  # noqa (pep8 N802)
        if mbd_is_ssl(request):
            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).addErrback(self._on_error, 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.debug(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, minthreads=0, maxthreads=10):
        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.thread_pool = twisted.python.threadpool.ThreadPool(minthreads, maxthreads)
        self.resource = RootResource(twisted.web.wsgi.WSGIResource(reactor, self.thread_pool, wsgi_app))
        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
        ep_errors = []

        def on_ep_error(f):
            ep_errors.append(f.value)

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

        for e in ep_errors:
            raise mini_buildd.HTTPUnavailable(str(e))

    def shutdown(self):
        self.thread_pool.stop()
        reactor.stop()

    def run(self):
        self.thread_pool.start()
        reactor.run(installSignalHandlers=0)
        LOG.debug(f"{self}: FINISHED")
