import logging
import os
import queue
from contextlib import closing

from mini_buildd import changes, config, daemon, events, net, sbuild, threads, util

LOG = logging.getLogger(__name__)


class Uploader(threads.EventThread):
    def __init__(self):
        super().__init__()

        self.failed = {}
        for bres_entry in (f for f in config.ROUTES["builds"].path.ifiles("", ".changes") if changes.Buildresult.match(f.path)):
            LOG.debug("Checking buildresult: %s", bres_entry.path)
            try:
                bres = changes.Buildresult(bres_entry.path)
                if not bres.uploaded():
                    LOG.warning("Found upload-pending buildresult: %s", bres_entry.path)
                    self.queue.put(bres)
            except Exception as e:
                LOG.error("Bogus buildresult (ignoring): %s: %s", bres_entry.path, e)

    def run_event(self, event):
        with self.lock:
            try:
                event.upload(net.ClientEndpoint(event.cget("Upload-To"), protocol="ftp"))
                self.failed.pop(event.bkey, None)
            except BaseException as e:
                util.log_exception(LOG, f"Upload to {event.cget('Upload-To')} failed (will retry)", e)
                self.failed[event.bkey] = event

    def retry_failed(self):
        with self.lock:
            for bres in self.failed.values():
                self.queue.put(bres)


class Build(threads.DeferredThread):
    def __init__(self, limiter, breq, uploader_queue):
        # Note: If the constructor fails, no buildresult would be uploaded (and packaging would hang). Keep it simple && be sure this does not fail on 'normal' error conditions
        self.breq = breq
        self.uploader_queue = uploader_queue
        self.sbuild = sbuild.SBuild(self.breq, ["--keyid", daemon.get().gnupg.get_first_sec_key()])
        super().__init__(limiter=limiter)

    def __str__(self):
        return f"{super().__str__()} ({self.breq.bkey})"

    def cancel(self, who):
        if self.is_alive():
            self.sbuild.cancel(who)

    def run_deferred(self):
        if self._shutdown is config.SHUTDOWN:
            raise util.HTTPShutdown

        daemon.get().events.log(events.Type.BUILDING, self.breq)
        try:
            # Verify buildrequest
            with closing(daemon.RemotesKeyring()) as gpg:
                gpg.verify(self.breq.file_path)

            # Run sbuild
            self.sbuild.run()

            build_changes_file = os.path.join(self.breq.dir_path(), self.breq.dfn.changes(arch=self.breq["architecture"]))
            self.sbuild.bres.save_to(self.breq.dir_path(), changes.Base(build_changes_file).tar() if os.path.exists(build_changes_file) else None)
        except BaseException as e:
            util.log_exception(LOG, f"Internal-Error: {self}", e)
            self.sbuild.bres.cset("Internal-Error", str(util.e2http(e)))
            self.sbuild.bres.save_to(self.breq.dir_path())

        daemon.get().events.log(events.Type.BUILT, self.sbuild.bres)
        self.uploader_queue.put(self.sbuild.bres)


class Builder(threads.EventThread):
    def __init__(self, max_parallel_builds):
        self.limiter = queue.Queue(maxsize=max_parallel_builds)
        self.builds = {}
        self.uploader = Uploader()
        super().__init__(subthreads=[self.uploader])

    def __str__(self):
        return f"{super().__str__()} ({self.limiter.maxsize} parallel builds)"

    def queued(self):
        return sum(map(lambda b: 1 if b.is_alive() else 0, self.builds.values()))

    def running(self):
        return sum(map(lambda b: 1 if (b.is_alive() and b.running is not None) else 0, self.builds.values()))

    def load(self):
        return round(float(self.queued() / self.limiter.maxsize), 2)

    def clear(self):
        with self.lock:
            for key in [key for key, build in self.builds.items() if not build.is_alive()]:  # Iterate on copy, del on actual dict
                self.builds[key].join()
                del self.builds[key]

    def join(self, timeout=None):
        for build in self.builds.values():
            build.cancel("SHUTDOWN")
            build.join()
        super().join()

    def run_event(self, event):
        self.clear()
        build = Build(self.limiter, event, self.uploader.queue)
        self.builds[build.breq.bkey] = build
        build.start()

    def cancel(self, bkey, who):
        self.clear()
        build = self.builds.get(bkey)
        if build is None:
            raise util.HTTPBadRequest(f"No such active build: {bkey}")
        build.cancel(who)
