import os
import json
import socket
import smtplib

from collections import defaultdict
from urllib.request import urlopen, URLError

from britney2 import SuiteClass
from britney2.policies.rest import Rest
from britney2.policies.policy import BasePolicy, PolicyVerdict


MESSAGE = """From: Ubuntu Stable Release Team <ubuntu-sru-bot@canonical.com>
To: {bug_mail}
Subject: Autopkgtest regression report ({source_name}/{version})

All autopkgtests for the newly accepted {source_name} ({version}) for {series_name} have finished running.
The following regressions have been reported in tests triggered by the package:

{failures}

Please visit the excuses page listed below and investigate the failures, proceeding afterwards as per the StableReleaseUpdates policy regarding autopkgtest regressions [1].

https://people.canonical.com/~ubuntu-archive/proposed-migration/{series_name}/update_excuses.html#{source_name}

[1] https://wiki.ubuntu.com/StableReleaseUpdates#Autopkgtest_Regressions

Thank you!
"""  # noqa:E501


class SRUADTRegressionPolicy(BasePolicy, Rest):
    def __init__(self, options, suite_info, dry_run=False):
        super().__init__(
            "sruadtregression",
            options,
            suite_info,
            {SuiteClass.PRIMARY_SOURCE_SUITE},
        )
        self.state_filename = os.path.join(
            options.unstable, "sru_regress_inform_state"
        )
        self.state = {}
        self.dry_run = dry_run
        self.email_host = getattr(self.options, "email_host", "localhost")

    def initialise(self, britney):
        super().initialise(britney)
        if os.path.exists(self.state_filename):
            with open(self.state_filename, encoding="utf-8") as data:
                self.state = json.load(data)
            self.logger.info("Loaded state file %s" % self.state_filename)
        # Remove any old entries from the statefile
        self.cleanup_state()

    def bugs_from_changes(self, change_url):
        """Return bug list from a .changes file URL"""
        last_exception = None
        # Querying LP can timeout a lot, retry 3 times
        for i in range(3):
            try:
                changes = urlopen(change_url)
                break
            except URLError as e:
                last_exception = e
                pass
        else:
            raise last_exception
        bugs = set()
        for ln in changes:
            ln = ln.decode("utf-8")
            if ln.startswith("Launchpad-Bugs-Fixed: "):
                bugs = {int(b) for b in ln.split()[1:]}
                break
        return bugs

    def apply_src_policy_impl(
        self, policy_info, item, source_data_tdist, source_data_srcdist, excuse
    ):
        source_name = item.package
        # We only care about autopkgtest regressions
        if (
            "autopkgtest" not in excuse.reason
            or not excuse.reason["autopkgtest"]
        ):
            return PolicyVerdict.PASS
        # Check the policy_info to see if all tests finished and, if yes, if we
        # have any failures to report on.
        if "autopkgtest" not in excuse.policy_info:
            return PolicyVerdict.PASS
        regressions = defaultdict(list)
        for pkg in excuse.policy_info["autopkgtest"]:
            # Unfortunately the verdict of the policy lives at the same level
            # as its detailed output
            if pkg == "verdict":
                continue
            for arch in excuse.policy_info["autopkgtest"][pkg]:
                if excuse.policy_info["autopkgtest"][pkg][arch][0] == "RUNNING":
                    # If there's at least one test still running, we just stop
                    # and not proceed any further.
                    return PolicyVerdict.PASS
                elif (
                    excuse.policy_info["autopkgtest"][pkg][arch][0]
                    == "REGRESSION"
                ):
                    regressions[pkg].append(arch)
        if not regressions:
            return PolicyVerdict.PASS

        version = source_data_srcdist.version
        distro_name = self.options.distribution
        series_name = self.options.series
        try:
            if self.state[distro_name][series_name][source_name] == version:
                # We already informed about the regression.
                return PolicyVerdict.PASS
        except KeyError:
            # Expected when no failure has been reported so far for this
            # source - we want to continue in that case. Easier than
            # doing n number of if-checks.
            pass
        data = self.query_lp_rest_api(
            "%s/+archive/primary" % distro_name,
            {
                "ws.op": "getPublishedSources",
                "distro_series": "/%s/%s" % (distro_name, series_name),
                "exact_match": "true",
                "order_by_date": "true",
                "pocket": "Proposed",
                "source_name": source_name,
                "version": version,
            },
        )
        try:
            src = next(iter(data["entries"]))
        # IndexError means no packages in -proposed matched this name/version.
        except StopIteration:
            self.logger.info(
                "No packages matching %s/%s the %s/%s main archive, not "
                "informing of ADT regressions"
                % (source_name, version, distro_name, series_name)
            )
            return PolicyVerdict.PASS
        changes_url = self.query_lp_rest_api(
            src["self_link"], {"ws.op": "changesFileUrl"}
        )
        if not changes_url:
            return PolicyVerdict.PASS
        # Prepare a helper string that lists all the ADT failures
        failures = ""
        for pkg, arches in regressions.items():
            failures += "%s (%s)\n" % (pkg, ", ".join(arches))

        bugs = self.bugs_from_changes(changes_url)
        # Now leave a comment informing about the ADT regressions on each bug
        for bug in bugs:
            self.logger.info(
                "%sSending ADT regression message to LP: #%s "
                "regarding %s/%s in %s"
                % (
                    "[dry-run] " if self.dry_run else "",
                    bug,
                    source_name,
                    version,
                    series_name,
                )
            )
            if not self.dry_run:
                try:
                    bug_mail = "%s@bugs.launchpad.net" % bug
                    server = smtplib.SMTP(self.email_host)
                    server.sendmail(
                        "noreply@canonical.com",
                        bug_mail,
                        MESSAGE.format(**locals()),
                    )
                    server.quit()
                except socket.error as err:
                    self.logger.info(
                        "Failed to send mail! Is SMTP server running?"
                    )
                    self.logger.info(err)
        self.save_progress(source_name, version, distro_name, series_name)
        return PolicyVerdict.PASS

    def save_progress(self, source, version, distro, series):
        if self.dry_run:
            return
        if distro not in self.state:
            self.state[distro] = {}
        if series not in self.state[distro]:
            self.state[distro][series] = {}
        self.state[distro][series][source] = version
        with open(self.state_filename, "w", encoding="utf-8") as data:
            json.dump(self.state, data)

    def cleanup_state(self):
        """Remove all no-longer-valid package entries from the statefile"""
        for distro_name in self.state:
            for series_name, pkgs in self.state[distro_name].items():
                for source_name, version in pkgs.copy().items():
                    data = self.query_lp_rest_api(
                        "%s/+archive/primary" % distro_name,
                        {
                            "ws.op": "getPublishedSources",
                            "distro_series": "/%s/%s"
                            % (distro_name, series_name),
                            "exact_match": "true",
                            "order_by_date": "true",
                            "pocket": "Proposed",
                            "status": "Published",
                            "source_name": source_name,
                            "version": version,
                        },
                    )
                    if not data["entries"]:
                        del self.state[distro_name][series_name][source_name]