"""
.. error:: **twisted < 24 + SSL** (``mbd_workaround_ssl``): Sporadic errors reading event queue

  Reproduce: ``./devel profile _ssl updatetestall`` eventually yields to some error reading events (nearly every time).

  With ``mbd_workaround_ssl`` in place, empirical data seems to suggest less errors, so this is still enabled
  for older versions.

  Extended empirical testing shows that all these problems are really **gone with twisted 24**.

.. error:: **twisted < 23.10.0** (``mbd_workaround_producer``): Twisted can't serve APT repository when HTTP pipelining is enabled (https://github.com/twisted/twisted/issues/11976)

  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.

  This error appears when APT has HTTP pipelining enabled (which it has by default) and reasonable big indices
  files. You may repeat this bug in plain twisted if you serve any reasonably sized repository like so::

    cd <your_repo_dir>
    twistd3 --nodaemon --pidfile /tmp/twistd.pid web --path $(pwd)
    ..
    EDIT /etc/apt/sources.list.d/test.list                           # Add resp. APT line
    sudo mini-buildd-internals sbuild-setup-blocks apt-clear --run   # Be sure error is not hidden by cached apt lines
    sudo apt update

  With ``mbd_workaround_producer`` in place makes APT pipelining work, so it's still in place
  for older versions. Fwiw, the worakround however most likely causes these seemingly random (but not
  practically breaking mini-buildd) errors::

    builtins.AttributeError: 'NoneType' object has no attribute 'unregisterProducer'
"""
import datetime
import json
import logging
import os.path

import twisted.internet.endpoints
import twisted.internet.threads
import twisted.python.log
import twisted.python.logfile
import twisted.python.threadpool
import twisted.web.resource
import twisted.web.server
import twisted.web.static
import twisted.web.wsgi
from twisted.internet import reactor

from mini_buildd import config, net, threads, util

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):
    """For workarounds only. Does not work with twisted 24"""
    return request.transport.__class__.__name__ == "TLSMemoryBIOProtocol"


class FileResource(twisted.web.static.File):
    """Twisted static resource"""

    NEEDS_PRODUCER_WORKAROUND = not util.VERSIONS.has("twisted", "23.10")

    @classmethod
    def mbd_workaround_producer(cls, request):
        try:
            if not mbd_is_ssl(request):
                # request.channel.unregisterProducer()
                request.channel.loseConnection()
        except BaseException as e:
            util.log_exception(LOG, "mbd_workaround_producer", e)

    def render(self, request):
        if self.NEEDS_PRODUCER_WORKAROUND:
            self.mbd_workaround_producer(request)

        # Note: If browser cache should be enabled at some point, then here. However, we have no "hashed-on-change" support and it's confusing if
        # 'though F5, js still running old code'. Also a real PITA in most browsers to clear the cache.
        # request.setHeader("Cache-Control", "public, max-age=60, no-transform")

        return super().render(request)


class Events(twisted.web.resource.Resource):
    NEEDS_SSL_WORKAROUND = not util.VERSIONS.has("twisted", "24")

    @classmethod
    def _render(cls, request):
        LOG.debug("%s: Waiting for event...", request.channel)
        after = datetime.datetime.fromisoformat(request.args[b"after"][0].decode(config.CHAR_ENCODING)) if b"after" in request.args else None
        queue = util.daemon().events.attach(request.channel, after=util.Datetime.check_aware(after))
        event = queue.get()
        LOG.debug("%s: Got event: %s", request.channel, event)
        if event is config.SHUTDOWN:
            raise util.HTTPShutdown
        request.write(json.dumps(event.to_json()).encode(config.CHAR_ENCODING))
        request.finish()

    @classmethod
    def _on_error(cls, failure, request):
        http_exception = util.e2http(failure.value)
        request.setResponseCode(http_exception.status, f"{http_exception}".encode(config.CHAR_ENCODING))
        if getattr(request, "_disconnected", False):
            request.notifyFinish()
        else:
            request.finish()

    @classmethod
    def mbd_workaround_ssl(cls, request):
        try:
            if mbd_is_ssl(request):
                request.channel.abortTimeout = None
                request.channel.setTimeout(None)
        except BaseException as e:
            util.log_exception(LOG, "mbd_workaround_ssl", e)

    def render_GET(self, request):  # noqa (pep8 N802)
        if self.NEEDS_SSL_WORKAROUND:
            self.mbd_workaround_ssl(request)

        request.setHeader("Content-Type", f"application/json; charset={config.CHAR_ENCODING}")
        twisted.internet.threads.deferToThread(self._render, request).addErrback(self._on_error, request)
        return twisted.web.server.NOT_DONE_YET


class HttpD(threads.Thread):
    def _add_route(self, uri, resource):
        """Add route from (possibly nested) path from config -- making sure already existing parent routes are reused"""
        LOG.debug("Adding route: %s -> %s", 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=config.CHAR_ENCODING), sub)
                self.route_hierarchy[path] = sub
            res = sub
        res.putChild(bytes(uri_split[-1], encoding=config.CHAR_ENCODING), resource)

    def __init__(self, wsgi_app, minthreads=0, maxthreads=10):
        # Use python's native logging system
        twisted.python.log.PythonLoggingObserver().start()

        self.endpoints = [net.ServerEndpoint(ep, protocol="http") for ep in config.HTTP_ENDPOINTS]
        super().__init__(name=",".join([ep.geturl() for ep in self.endpoints]))
        self.route_hierarchy = {}

        # 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=config.ROUTES["log"].path.join(config.ACCESS_LOG_FILE))

        # Static routes
        for route in config.ROUTES.values():
            for uri in (uri for key, uri in route.uris.items() if key.startswith("static")):
                self._add_route(uri, FileResource(route.path.full, defaultType=f"text/plain; charset={config.CHAR_ENCODING}"))

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

        # Start sockets
        ep_errors = []

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

        for ep in self.endpoints:
            twisted.internet.endpoints.serverFromString(reactor, ep.description).listen(self.site).addErrback(on_ep_error)

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

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

    def mbd_run(self):
        self.thread_pool.start()
        reactor.run(installSignalHandlers=0)
