import sys
import time
import pickle
import urllib.request
import urllib.parse
import urllib.error
import re

import debian.debian_support

import mini_buildd.misc
import mini_buildd.net
import mini_buildd.api


class Daemon():
    def _log(self, message):
        print(f"{self.host}: {message}", file=sys.stderr)

    def _log_daemon_messages(self, headers):
        """Log daemon messages (stolen from mini-buildd-tool)."""
        msgs_header = "x-mini-buildd-message"
        for msg in [v for k, v in list(headers.items()) if msgs_header == k[:len(msgs_header)]]:
            self._log(f"HTTP Header Message: {mini_buildd.misc.b642u(msg)}")

    def __init__(self, host, port="8066", proto="http",
                 auto_confirm=False,
                 dry_run=False,
                 batch_mode=False,
                 django_mode=True):
        self.host = host
        self.port = port
        self.proto = proto
        self.url = f"{proto}://{host}:{port}"
        self.api_url = f"{self.url}/mini_buildd/api"
        self.auto_confirm = auto_confirm
        self.dry_run = dry_run
        self.batch_mode = batch_mode
        if django_mode:
            mini_buildd.api.django_pseudo_configure()

        # Extra: status caching
        self._status = None
        # Extra: dputconf caching (for archive identity workaround)
        self._dputconf = None

    def login(self, user=None):
        """Login. Use the user's mini-buildd keyring for auth, like mini-buildd-tool."""
        keyring = mini_buildd.misc.Keyring("mini-buildd")
        mini_buildd.net.ClientEndpoint(f"{self.proto}://{user if user or self.batch_mode else input('Username: ')}@{self.host}:{self.port}").web_login(keyring)
        return self

    def call(self, command, args=None, output="python", raise_on_error=True):
        if args is None:
            args = {}

        if self.auto_confirm:
            args["confirm"] = command
        http_get_args = "&".join(f"{k}={v}" for k, v in list(args.items()))
        url = f"{self.api_url}?command={command}&output={output}&{http_get_args}"

        if self.dry_run:
            self._log(f"Dry Run, skipping API: {url}")
            return None

        self._log(f"Calling API: {url}")
        try:
            response = mini_buildd.net.urlopen(url)
            return pickle.loads(response.read()) if output == "python" else response.read()
        except urllib.error.HTTPError as e:
            self._log(f"API call failed with HTTP Status {e.getcode()}:")
            self._log_daemon_messages(e.headers)
            if not self.batch_mode and e.getcode() == 401:
                action = input("Unauthorized retry: (l)ogin, (c)onfirm this call or (C)onfirm all future calls (anything else to just skip)? ")
                if action and action in "lcC":
                    new_args = args
                    if action == "l":
                        self.login()
                    elif action == "c":
                        new_args["confirm"] = command
                    elif action == "C":
                        self.auto_confirm = True
                    return self.call(command, new_args, output=output, raise_on_error=raise_on_error)
            if raise_on_error:
                raise

    # Extra functionality
    @property
    def identity(self):
        """Get Archive's Identity."""
        # Bug 1.0.x: "status" does not give the archive id.
        # Workaround: Parse the archive id from dput.cf
        if self._dputconf is None:
            self._dputconf = self.call("getdputconf")
        # 1st line looks like: "[mini-buildd-my-archive-id]"
        dput_target = self._dputconf._plain_result.split("\n", 1)[0].rpartition("]")[0]  # pylint: disable=protected-access
        return dput_target[len("mini-buildd-") + 1:]

    @property
    def status(self):
        if self._status is None:
            self._status = self.call("status")
        return self._status

    @property
    def repositories(self):
        return list(self.status.repositories.keys())

    def get_codenames(self, repo):
        return self.status.repositories[repo]

    def get_package_versions(self, src_package, dist_regex=".*"):
        """Produce a dict with all (except rollback) available versions of this package (key=distribution, value=info dict: version, dsc_url, log_url, changes_url*)."""
        def _base_url(url):
            url_split = urllib.parse.urlsplit(url)
            return f"{url_split.scheme}://{url_split.hostname}:{url_split.port}"

        show = self.call("show", {"package": src_package})
        result = {}
        for repository in show.repositories:
            for versions in repository[1]:
                for version in versions[1]:
                    dist = version["distribution"]
                    vers = version["sourceversion"]
                    # Note: 'vers' may be empty when only rollbacks exist
                    if vers and re.match(dist_regex, dist):
                        info = {}

                        repository = mini_buildd.misc.Distribution(dist).repository

                        info["version"] = vers
                        info["dsc_url"] = version["dsc_url"]
                        base_url = _base_url(info["dsc_url"])
                        info["log_url"] = f"{base_url}/mini_buildd/log/{repository}/{src_package}/{vers}/"
                        # Note: Path may also be "/source all/", not "/source/" (on non-source-only uploads?)
                        info["changes_url"] = f"{base_url}/log/{repository}/{src_package}/{vers}/source/{src_package}_{vers}_source.changes"
                        result[dist] = info

        return result

    def wait_for_package(self, distribution, src_package, version=None, or_greater=False,  # pylint: disable=inconsistent-return-statements
                         max_tries=-1, sleep=60, initial_sleep=0,
                         raise_on_error=True):
        """Block until a specific package is in repository."""
        item = f"\"{src_package}_{version}\" in \"{distribution}\""

        def _sleep(secs):
            self._log(f"Waiting for {item}: Idling {secs} seconds (Ctrl-C to abort)...")
            time.sleep(secs)

        tries = 0
        _sleep(initial_sleep)
        while max_tries < 0 or tries < max_tries:
            pkg_info = self.get_package_versions(src_package, distribution).get(distribution, {})
            actual_version = pkg_info.get("version", None)
            self._log(f"Actual version for {item}: {actual_version}")

            # pylint: disable=too-many-boolean-expressions
            if (version is None and actual_version) or \
               (version is not None and (actual_version == version or or_greater and debian.debian_support.Version(actual_version) >= debian.debian_support.Version(version))):
                self._log(f"Match found: {item}.")
                return pkg_info
            _sleep(sleep)
            tries += 1

        not_found_msg = f"Could not find {item} within {initial_sleep + tries * sleep} seconds."
        self._log(not_found_msg)
        if raise_on_error:
            raise Exception(not_found_msg)

    def has_package(self, distribution, src_package, version=None, or_greater=False):
        """Check if a specific package is in repository."""
        return self.wait_for_package(distribution, src_package, version, or_greater=or_greater,
                                     max_tries=1, sleep=0, initial_sleep=0,
                                     raise_on_error=False)

    def bulk_migrate(self, packages, repositories=None, codenames=None, suites=None):
        """Bulk-migrate a package over repositories, base distributions and suites."""
        status = self.call("status")

        if repositories is None:
            repositories = list(status.repositories.keys())
        if suites is None:
            suites = ["unstable", "testing"]

        for package in packages:
            for repository in repositories:
                iter_codenames = codenames
                if iter_codenames is None:
                    iter_codenames = list(status.repositories[repository])
                for codename in iter_codenames:
                    for suite in suites:
                        dist = f"{codename}-{repository}-{suite}"
                        self.call("migrate", {"package": package, "distribution": dist}, raise_on_error=False)
