"""
Ported to Python 3.
"""

import time
from hyperlink import (
    DecodedURL,
)
from twisted.web.template import (
    renderer,
    tags as T,
)
from twisted.python.urlpath import (
    URLPath,
)
from twisted.python.failure import Failure
from twisted.internet import reactor, defer
from twisted.web import resource
from twisted.web.http import NOT_FOUND
from twisted.web.html import escape
from twisted.application import service

from allmydata.web.common import (
    WebError,
    get_arg,
    boolean_of_arg,
    exception_to_child,
)

MINUTE = 60
HOUR = 60*MINUTE
DAY = 24*HOUR

(MONITOR, RENDERER, WHEN_ADDED) = range(3)

class OphandleTable(resource.Resource, service.Service):
    """Renders /operations/%d."""
    # The type in Twisted for services is wrong in 22.10...
    # https://github.com/twisted/twisted/issues/10135
    name = "operations"  # type: ignore[assignment]

    UNCOLLECTED_HANDLE_LIFETIME = 4*DAY
    COLLECTED_HANDLE_LIFETIME = 1*DAY

    def __init__(self, clock=None):
        super(OphandleTable, self).__init__()
        # both of these are indexed by ophandle
        self.handles = {} # tuple of (monitor, renderer, when_added)
        self.timers = {}
        # The tests will provide a deterministic clock
        # (twisted.internet.task.Clock) that they can control so that
        # they can test ophandle expiration. If this is provided, I'll
        # use it schedule the expiration of ophandles.
        self.clock = clock

    def stopService(self):
        for t in self.timers.values():
            if t.active():
                t.cancel()
        del self.handles # this is not restartable
        del self.timers
        return service.Service.stopService(self)

    def add_monitor(self, req, monitor, renderer):
        """
        :param allmydata.webish.MyRequest req:
        :param allmydata.monitor.Monitor monitor:
        :param allmydata.web.directory.ManifestResults renderer:
        """
        ophandle = get_arg(req, "ophandle")
        assert ophandle
        now = time.time()
        self.handles[ophandle] = (monitor, renderer, now)
        retain_for = get_arg(req, "retain-for", None)
        if retain_for is not None:
            self._set_timer(ophandle, int(retain_for))
        monitor.when_done().addBoth(self._operation_complete, ophandle)

    def _operation_complete(self, res, ophandle):
        if ophandle in self.handles:
            if ophandle not in self.timers:
                # the client has not provided a retain-for= value for this
                # handle, so we set our own.
                now = time.time()
                added = self.handles[ophandle][WHEN_ADDED]
                when = max(self.UNCOLLECTED_HANDLE_LIFETIME, now - added)
                self._set_timer(ophandle, when)
            # if we already have a timer, the client must have provided the
            # retain-for= value, so don't touch it.

    def redirect_to(self, req):
        """
        :param allmydata.webish.MyRequest req:
        """
        ophandle = get_arg(req, "ophandle").decode("utf-8")
        assert ophandle
        here = DecodedURL.from_text(str(URLPath.fromRequest(req)))
        target = here.click(u"/").child(u"operations", ophandle)
        output = get_arg(req, "output")
        if output:
            target = target.add(u"output", output.decode("utf-8"))
        return target

    @exception_to_child
    def getChild(self, name, req):
        ophandle = name
        if ophandle not in self.handles:
            raise WebError("unknown/expired handle '%s'" % escape(str(ophandle, "utf-8")),
                           NOT_FOUND)
        (monitor, renderer, when_added) = self.handles[ophandle]

        t = get_arg(req, "t", "status")
        if t == b"cancel" and req.method == b"POST":
            monitor.cancel()
            # return the status anyways, but release the handle
            self._release_ophandle(ophandle)

        else:
            retain_for = get_arg(req, "retain-for", None)
            if retain_for is not None:
                self._set_timer(ophandle, int(retain_for))

            if monitor.is_finished():
                if boolean_of_arg(get_arg(req, "release-after-complete", "false")):
                    self._release_ophandle(ophandle)
                if retain_for is None:
                    # this GET is collecting the ophandle, so change its timer
                    self._set_timer(ophandle, self.COLLECTED_HANDLE_LIFETIME)

        status = monitor.get_status()
        if isinstance(status, Failure):
            return defer.fail(status)

        return renderer

    def _set_timer(self, ophandle, when):
        if ophandle in self.timers and self.timers[ophandle].active():
            self.timers[ophandle].cancel()
        if self.clock:
            t = self.clock.callLater(when, self._release_ophandle, ophandle)
        else:
            t = reactor.callLater(when, self._release_ophandle, ophandle)
        self.timers[ophandle] = t

    def _release_ophandle(self, ophandle):
        if ophandle in self.timers and self.timers[ophandle].active():
            self.timers[ophandle].cancel()
        self.timers.pop(ophandle, None)
        self.handles.pop(ophandle, None)


class ReloadMixin(object):
    REFRESH_TIME = 1*MINUTE

    @renderer
    def refresh(self, req, tag):
        if self.monitor.is_finished():
            return ""
        tag.attributes["http-equiv"] = "refresh"
        tag.attributes["content"] = str(self.REFRESH_TIME)
        return tag

    @renderer
    def reload(self, req, tag):
        if self.monitor.is_finished():
            return b""
        # url.gethere would break a proxy, so the correct thing to do is
        # req.path[-1] + queryargs
        ophandle = req.prepath[-1]
        reload_target = ophandle + b"?output=html"
        cancel_target = ophandle + b"?t=cancel"
        cancel_button = T.form(T.input(type="submit", value="Cancel"),
                               action=cancel_target,
                               method="POST",
                               enctype="multipart/form-data",)

        return (T.h2("Operation still running: ",
                     T.a("Reload", href=reload_target),
                     ),
                cancel_button,)
