import os
import queue
import logging
from contextlib import closing

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


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(f"Checking buildresult: {bres_entry.path}")
            try:
                bres = changes.Buildresult(bres_entry.path)
                if not bres.uploaded():
                    LOG.warning(f"Found upload-pending buildresult: {bres_entry.path}")
                    self.queue.put(bres)
            except Exception as e:
                LOG.error(f"Bogus buildresult (ignoring): {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.bres = self.breq.gen_buildresult()
        self.sbuild = None
        super().__init__(limiter=limiter, name=self.breq.bkey)

    def cancel(self, who):
        if self.is_alive() and self.sbuild is not None:
            self.bres.cset("Canceled-By", who)
            self.sbuild.cancel()

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

        util.daemon().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 = sbuild.SBuild(self.breq)
            self.sbuild.run(self.bres)
            LOG.debug(f"{self}: Sbuild finished: {self.bres.cget('Sbuild-Status')}")

            build_changes_file = self.breq.builds_path.join(self.breq.dfn.changes(arch=self.breq["architecture"]))
            self.bres.save_to(self.bres.builds_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.bres.cset("Internal-Error", str(util.e2http(e)))
            self.bres.save_to(self.bres.builds_path)

        util.daemon().events.log(events.Type.BUILT, self.bres)
        self.uploader_queue.put(self.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], name=f"{max_parallel_builds} 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)
