"""
.. error:: **twisted+SSL**: IncompleteRead on event queue

  Reproduce: ``./devel profile _ssl updatetestall`` eventually yields to error
  ``IncompleteRead(<n> bytes read)`` (nearly every time).

.. error:: **twisted+SSL**: ``apt-get update`` form a ``squeeze`` platform will stall ``twisted``

  ``squeeze`` should not really be used nowadays, however no HTTP(S) request should make twisted stall. Still have not
  broken down what exactly APT@squeeze does differently for https.

.. attention:: **twisted+SSL** (``mbd_workaround_ssl``): 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.

.. attention:: **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)

  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.

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

from mini_buildd import config, util, threads, net

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):
    return request.transport.__class__.__name__ == "TLSMemoryBIOProtocol"


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

    @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):
        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):
    @classmethod
    def _render(cls, request):
        LOG.debug(f"{request.channel}: Waiting for event...")
        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(f"{request.channel}: Got event: {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)
        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 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=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)
