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 Uploader(mini_buildd.threads.EventThread):
    def __init__(self):
        super().__init__(name="uploader")

        self.failed = []
        for bres_entry in (f for f in mini_buildd.config.ROUTES["builds"].path.ifiles("", ".changes") if mini_buildd.changes.Buildresult.match(f.path)):
            LOG.debug(f"Checking buildresult: {bres_entry.path}")
            try:
                bres = mini_buildd.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(mini_buildd.net.ClientEndpoint(event.cget("Upload-To"), protocol="ftp"))
                mini_buildd.get_daemon().events.log(mini_buildd.events.Type.BUILT, event)
                if event in self.failed:
                    self.failed.remove(event)
            except BaseException as e:
                mini_buildd.log_exception(LOG, f"Upload to {event.cget('Upload-To')} failed (will retry)", e)
                if event not in self.failed:
                    self.failed.append(event)

    def retry_failed(self):
        with self.lock:
            failed = self.failed.copy()  # Note: afaik, copy() is atomic in cpython >= 3.5, but not generally
        for bres in failed:
            self.queue.put(bres)


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, breq, uploader_queue):
        self.breq = breq
        self.uploader_queue = uploader_queue
        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, 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 mini_buildd.config.SHUTDOWN:
            raise mini_buildd.HTTPShutdown

        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)

        self.uploader_queue.put(self.bres)


class Builder(mini_buildd.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="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("SHUTDOWN")
            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.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 mini_buildd.HTTPBadRequest(f"No such active build: {bkey}")
        build.cancel(who)
