import os
import queue
import logging
from contextlib import closing

import mini_buildd.config
import mini_buildd.schroot
import mini_buildd.sbuild
import mini_buildd.threads
import mini_buildd.net
import mini_buildd.changes


LOG = logging.getLogger(__name__)


class Build(mini_buildd.threads.DeferredThread):
    """
    .. important:: 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.
    """

    def __init__(self, limiter, event):
        self.breq = mini_buildd.changes.Buildrequest(event, create_events=True, create_builds=True)
        self.bres = self.breq.gen_buildresult()
        self.sbuild = None
        super().__init__(limiter=limiter, name=f"Building {self.breq.bkey}")

    def __str__(self):
        return self.breq.bkey

    def cancel(self):
        if self.is_alive() and self.sbuild is not None:
            self.sbuild.cancel()

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

        # Build if needed (may be just an upload-pending build)
        if self.bres.needs_build():
            mini_buildd.get_daemon().events.log(mini_buildd.events.Type.BUILDING, self.breq)
            try:
                # Verify buildrequest
                with closing(mini_buildd.daemon.RemotesKeyring()) as gpg:
                    gpg.verify(self.breq.file_path)

                # Run sbuild
                self.sbuild = mini_buildd.sbuild.SBuild(self.breq)
                self.sbuild.run(self.bres)
                LOG.info(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, mini_buildd.changes.Base(build_changes_file).tar() if os.path.exists(build_changes_file) else None)
            except BaseException as e:
                mini_buildd.log_exception(LOG, f"Internal-Error: {self}", e)
                self.bres.cset("Internal-Error", str(mini_buildd.e2http(e)))
                self.bres.save_to(self.bres.builds_path)

        if self.bres.needs_upload():
            try:
                self.bres.upload(mini_buildd.net.ClientEndpoint(self.breq.cget("Upload-To"), protocol="ftp"))
                # Unless this is done, buildresult stays in incoming and may be retried.
                self.breq.move_to(self.breq.builds_path)
                mini_buildd.get_daemon().events.log(mini_buildd.events.Type.BUILT, self.bres)
            except BaseException as e:
                mini_buildd.log_exception(LOG, f"Upload to {self.breq.cget('Upload-To')} failed (will retry)", e)


class Builder(mini_buildd.threads.EventThread):
    def __init__(self, max_parallel_builds):
        self.queue = queue.Queue()
        self.limiter = queue.Queue(maxsize=max_parallel_builds)
        self.builds = {}
        super().__init__(events=self.queue, name="builder")

    def __str__(self):
        return f"Building {len(self.alive())}/{self.limiter.maxsize} (load {self.load()}, {len(self.builds)} threads)"

    def alive(self):
        return [build for build in self.builds.values() if build.is_alive()]

    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()
            build.join()
        super().join()

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

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

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