"""
Distribution setups && support.

Dist-like variable naming convention
====================================
* ``diststr``: Instance of ``str``: "buster-test-unstable".
* ``dist``: Instance of ``mini_buildd.dist.Dist``: diststr parsed && support.
* ``distribution``: Instance of ``mini_buildd.model.distribution.Distribution``: Configured distribution.
"""

import enum
import re
import logging

import distro_info

import mini_buildd
from mini_buildd.sbuild import SETUP_BLOCKS

LOG = logging.getLogger(__name__)


class DebianDistroInfo(distro_info.DebianDistroInfo):
    def mbd_lts(self):
        """
        .. tip:: What does ``LTS`` (mini-buildd speak) include for Debian?

           For mini-buildd, ``LTS`` means ``Debian Long Term Support (LTS)`` including ``Debian Extended Long Term Support (ELTS)``.

           See: https://www.debian.org/lts/, https://wiki.debian.org/LTS/Extended
        """
        return self.lts_supported() + self.elts_supported()


class UbuntuDistroInfo(distro_info.UbuntuDistroInfo):
    def mbd_lts(self):
        """
        .. tip:: What does ``LTS`` (mini-buildd speak) include for Ubuntu?

           For mini-buildd, ``LTS`` means ``Ubuntu Long Term Support (LTS)`` including ``Ubuntu Extended Security Maintenance (ESM)``.

           See: https://ubuntu.com/about/release-cycle
        """
        return self.supported_esm()  # this includes LTS


class SourceSetup:
    """
    .. attention:: **compat** (``python < 3.7``): Not using namedtuple w/ defaults.
    """

    def __init__(self, origin, codename, apt_keys, extra_options=""):
        self.origin, self.codename, self.apt_keys, self.extra_options = origin, codename, apt_keys, extra_options


APT_KEYS = {
    "Debian": {
        "bullseye": {
            "archive": "73A4F27B8DD47936",    # Debian Archive Automatic Signing Key (11/bullseye) <ftpmaster@debian.org>
            "release": "605C66F00D6C9793",    # Debian Stable Release Key (11/bullseye) <debian-release@lists.debian.org>
            "security": "A48449044AAD5C5D",   # Debian Security Archive Automatic Signing Key (11/bullseye) <ftpmaster@debian.org>
        },
        "buster": {
            "archive": "DC30D7C23CBBABEE",    # Debian Archive Automatic Signing Key (10/buster) <ftpmaster@debian.org>
            "release": "DCC9EFBF77E11517",    # Debian Stable Release Key (10/buster) <debian-release@lists.debian.org>
            "security": "4DFAB270CAA96DFA",   # Debian Security Archive Automatic Signing Key (10/buster) <ftpmaster@debian.org>
        },
        "stretch": {
            "archive": "E0B11894F66AEC98",    # Debian Archive Automatic Signing Key (9/stretch) <ftpmaster@debian.org>  (subkey 04EE7237B7D453EC)
            "release": "EF0F382A1A7B6500",    # Debian Stable Release Key (9/stretch) <debian-release@lists.debian.org>
            "security": "EDA0D2388AE22BA9",   # Debian Security Archive Automatic Signing Key (9/stretch) <ftpmaster@debian.org>
        },
        "jessie": {
            "archive": "7638D0442B90D010",    # Debian Archive Automatic Signing Key (8/jessie) <ftpmaster@debian.org>
            "release": "CBF8D6FD518E17E1",    # Jessie Stable Release Key <debian-release@lists.debian.org>
            "security": "9D6D8F6BC857C906",   # Debian Security Archive Automatic Signing Key (8/jessie) <ftpmaster@debian.org>
        },
        "wheezy": {
            "archive": "8B48AD6246925553",    # Debian Archive Automatic Signing Key (7.0/wheezy) <ftpmaster@debian.org>
            "release": "6FB2A1C265FFB764",    # Wheezy Stable Release Key <debian-release@lists.debian.org>
        },
        "squeeze": {
            "archive": "AED4B06F473041FA",    # Debian Archive Automatic Signing Key (6.0/squeeze) <ftpmaster@debian.org>
            "release": "64481591B98321F9",    # Squeeze Stable Release Key <debian-release@lists.debian.org>
        },
        "lenny": {
            "archive": "9AA38DCD55BE302B",    # Debian Archive Automatic Signing Key (5.0/lenny) <ftpmaster@debian.org>
            "release": "4D270D06F42584E6",    # Lenny Stable Release Key <debian-release@lists.debian.org>
        },
    },
    "Ubuntu": {
        "current": "40976EAF437D05B5",   # Ubuntu Archive Automatic Signing Key <ftpmaster@ubuntu.com>
        "2012": "3B4FE6ACC0B21F32",      # Ubuntu Archive Automatic Signing Key (2012) <ftpmaster@ubuntu.com>
        "2018": "871920D1991BC93C",      # Ubuntu Archive Automatic Signing Key (2018) <ftpmaster@ubuntu.com>
    },
}

SUITE_SETUP = {
    "stable": {
        "rollback": 6,
        "options": {
            "uploadable": False,
        },
    },
    "hotfix": {
        "migrates_to": "stable",
        "rollback": 4,
    },
    "testing": {
        "migrates_to": "stable",
        "rollback": 3,
        "options": {
            "uploadable": False,
        },
    },
    "unstable": {
        "migrates_to": "testing",
        "rollback": 9,
        "options": {
            "build_keyring_package": True,
        },
    },
    "snapshot": {
        "rollback": 12,
        "options": {
            "experimental": True,
        },
    },
    "experimental": {
        "rollback": 6,
        "options": {
            "experimental": True,
            "but_automatic_upgrades": False,
        },
    },
}

SETUP = {
    #: Default (bullseye and up, groovy and up) lintian options. Overwrite in codename sections if needed.
    "lintian_options": {
        "common": ["--suppress-tags", "bad-distribution-in-changes-file"],
        #: Lintian "warnfail" option history:
        #:  * Debian (stretch)  2.5.50: Deprecates '--fail-on-warnings'.
        #:  * Debian (bullseye) 2.16.0: Drops support for '--fail-on-warnings' (NO WAY TO DO "WARNFAIL").
        #:  * Debian (bullseye) 2.77.0: Introduces '--fail-on'.
        #:  * Ubuntu (focal)    2.62.0: Release that does not have either (NO WAY TO DO "WARNFAIL").
        #:  * Ubuntu (groovy)   2.89.0: 1st release >= 2.77.
        #: Conclusion:
        #:  * Debian >=bullseye needs '--fail-on error,warning', below needs '--fail-on-warnings'.
        #:  * Ubuntu >=groovy needs '--fail-on error,warning', below needs '--fail-on-warnings'.
        #:  * Ubuntu focal just can't do it.
        "warnfail": ["--fail-on", "error,warning"],
    },

    "vendor": {
        "Debian": {
            #: distro info (unused for now)
            "distro_info": DebianDistroInfo(),

            #: "Usually used" paths on a mirror. Can be used to guess individual archive URLS from a base mirror URL.
            "archive_paths": ["debian", "debian-security", "debian-ports", "debian-archive/debian", "debian-archive/debian-security", "debian-archive/debian-backports"],

            #: Canonical (internet) archives
            "archive": [
                "http://ftp.debian.org/debian/",                 # Debian (release, updates, proposed-updates and backports)
                "http://deb.debian.org/debian/",                 # alternate: CDN

                "http://security.debian.org/debian-security/",   # Debian Security (release/updates)
                "http://deb.debian.org/debian-security/",        # alternate: CDN

                "http://archive.debian.org/debian/",             # Archived Debian Releases
                "http://archive.debian.org/debian-security/",    # Archived Debian Security
                "http://archive.debian.org/debian-backports/",   # Archived Debian Backports
            ],
            "default_components": ["main", "contrib", "non-free"],

            #: Security: Debian uses ``<codename>-security`` since "bullseye". Before that, it used strange ``<codename>/updates``.
            "security_codename_regex": re.compile(r"(^[a-z]+-security$|^[a-z]+/updates$)"),

            #: Known Debian sources, default setup.
            #: To display the key ids via apt-key in the format as used here::
            #:   apt-key adv --list-public-keys --keyid-format=long
            "codename": {
                "sid": {
                    "sources": [
                        SourceSetup("Debian", "sid", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                        SourceSetup("Debian", "experimental", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                    ],
                },
                "bullseye": {
                    "sources": [
                        SourceSetup("Debian", "bullseye", [APT_KEYS["Debian"]["stretch"]["archive"], APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"], APT_KEYS["Debian"]["bullseye"]["release"]]),
                        SourceSetup("Debian", "bullseye-security", [APT_KEYS["Debian"]["buster"]["security"], APT_KEYS["Debian"]["bullseye"]["security"]],
                                    "X-Remove-From-Component: updates/"),
                        SourceSetup("Debian Backports", "bullseye-backports", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                    ],
                },
                "buster": {
                    "sources": [
                        SourceSetup("Debian", "buster", [APT_KEYS["Debian"]["stretch"]["archive"], APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"], APT_KEYS["Debian"]["buster"]["release"]]),
                        SourceSetup("Debian", "buster/updates", [APT_KEYS["Debian"]["buster"]["security"], APT_KEYS["Debian"]["bullseye"]["security"]],
                                    extra_options="Codename: buster\nLabel: Debian-Security\nX-Remove-From-Component: updates/"),
                        SourceSetup("Debian Backports", "buster-backports", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                        SourceSetup("Debian Backports", "buster-backports-sloppy", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                    ],
                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },
                },
                "stretch": {
                    "sources": [
                        SourceSetup("Debian", "stretch", [APT_KEYS["Debian"]["stretch"]["archive"], APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"], APT_KEYS["Debian"]["stretch"]["release"]]),
                        SourceSetup("Debian", "stretch/updates", [APT_KEYS["Debian"]["buster"]["security"], APT_KEYS["Debian"]["stretch"]["security"]],
                                    extra_options="Codename: stretch\nLabel: Debian-Security\nX-Remove-From-Component: updates/"),
                        SourceSetup("Debian Backports", "stretch-backports", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                        SourceSetup("Debian Backports", "stretch-backports-sloppy", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                    ],
                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },
                },
                "jessie": {
                    # jessie sources 'archive.debian.org' and 'ftp.debian.org' are not the same:
                    # * jessie from archive.debian.org needs this setting (as the Release file is signed with an expired key, and jessie's apt just won't work).
                    # * jessie from ftp.debian.org (was left there for LTS for 'amd64 armel armhf i386' only) would work w/o that flag, as it is signed differently.
                    # However, both archives will match the jessie source, and both could potentially be used.
                    "apt_allow_unauthenticated": True,

                    "sources": [
                        SourceSetup("Debian", "jessie", [APT_KEYS["Debian"]["wheezy"]["archive"], APT_KEYS["Debian"]["jessie"]["release"], APT_KEYS["Debian"]["jessie"]["archive"]]),
                        SourceSetup("Debian", "jessie/updates", [APT_KEYS["Debian"]["jessie"]["security"], APT_KEYS["Debian"]["stretch"]["security"]],
                                    "Codename: jessie\nLabel: Debian-Security\nX-Remove-From-Component: updates/"),
                        SourceSetup("Debian Backports", "jessie-backports", [APT_KEYS["Debian"]["wheezy"]["archive"], APT_KEYS["Debian"]["jessie"]["archive"]],
                                    "X-Check-Valid-Until: no"),
                        SourceSetup("Debian Backports", "jessie-backports-sloppy", [APT_KEYS["Debian"]["wheezy"]["archive"], APT_KEYS["Debian"]["jessie"]["archive"]],
                                    "X-Check-Valid-Until: no"),
                    ],
                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },
                    "extra_options": SETUP_BLOCKS.line("apt-binary-keys apt-no-check-valid-until", top=True),
                },
                "wheezy": {
                    "sources": [
                        SourceSetup("Debian", "wheezy", [APT_KEYS["Debian"]["wheezy"]["archive"], APT_KEYS["Debian"]["wheezy"]["release"], APT_KEYS["Debian"]["jessie"]["archive"]]),
                        SourceSetup("Debian", "wheezy/updates", [APT_KEYS["Debian"]["wheezy"]["archive"], APT_KEYS["Debian"]["jessie"]["security"]],
                                    "Codename: wheezy\nLabel: Debian-Security\nX-Remove-From-Component: updates/\nX-Check-Valid-Until: no"),
                        SourceSetup("Debian Backports", "wheezy-backports", [APT_KEYS["Debian"]["wheezy"]["archive"], APT_KEYS["Debian"]["wheezy"]["release"], APT_KEYS["Debian"]["jessie"]["archive"]]),
                        SourceSetup("Debian Backports", "wheezy-backports-sloppy", [APT_KEYS["Debian"]["wheezy"]["archive"], APT_KEYS["Debian"]["jessie"]["archive"]]),
                    ],
                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },
                    "extra_options": SETUP_BLOCKS.line("apt-no-check-valid-until", top=True),
                },
                "squeeze": {
                    "broken": "apt update works, package builds fail",

                    "sources": [
                        SourceSetup("Debian", "squeeze", [APT_KEYS["Debian"]["squeeze"]["archive"], APT_KEYS["Debian"]["squeeze"]["release"]]),
                        SourceSetup("Debian", "squeeze/updates", [APT_KEYS["Debian"]["squeeze"]["archive"], APT_KEYS["Debian"]["jessie"]["security"]],
                                    "Codename: squeeze\nLabel: Debian-Security\nX-Remove-From-Component: updates/\nX-Check-Valid-Until: no"),
                        SourceSetup("Debian Backports", "squeeze-backports", [APT_KEYS["Debian"]["squeeze"]["archive"], APT_KEYS["Debian"]["wheezy"]["archive"]]),
                    ],
                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },
                    "extra_options": SETUP_BLOCKS.line("apt-key-add apt-no-check-valid-until apt-clear apt-urold", top=True),
                },
                "lenny": {
                    "broken": "apt update works, package builds fail",

                    # Avoid using the '--suppress-tags' lintian option for urold dists.
                    "lintian_options": {
                        "common": [],
                        "warnfail": ["--fail-on-warnings"],
                    },
                    "needs_uname_26": True,
                    "sources": [
                        SourceSetup("Debian", "lenny", [APT_KEYS["Debian"]["squeeze"]["archive"], APT_KEYS["Debian"]["lenny"]["release"]]),
                        SourceSetup("Debian", "lenny/updates", [APT_KEYS["Debian"]["lenny"]["archive"], APT_KEYS["Debian"]["jessie"]["security"]],
                                    "Codename: lenny\nLabel: Debian-Security\nX-Remove-From-Component: updates/\nX-Check-Valid-Until: no"),
                        SourceSetup("Debian Backports", "lenny-backports", [APT_KEYS["Debian"]["squeeze"]["archive"]]),
                    ],
                    "extra_options": SETUP_BLOCKS.line("apt-key-add apt-no-check-valid-until apt-clear apt-urold", top=True),
                },
            },
        },
        #: Ubuntu (https://wiki.ubuntu.com/Releases)
        #:
        #: Keep all active LTS and the four most recent releases.
        "Ubuntu": {
            "distro_info": UbuntuDistroInfo(),
            "archive_paths": ["ubuntu", "ubuntu-old"],
            "archive": [
                "http://archive.ubuntu.com/ubuntu/",             # Ubuntu releases
                "http://security.ubuntu.com/ubuntu/",            # Ubuntu Security
                "http://old-releases.ubuntu.com/ubuntu/",        # Older Ubuntu release
            ],
            "default_components": ["main", "universe", "restricted", "multiverse"],

            #: Security: Ubuntu uses ``<codename>-security``.
            "security_codename_regex": re.compile(r"^[a-z]+-security$"),

            "codename": {
                "jammy": {
                    "arch_optional": ["i386"],
                    "sources": [  # jammy: 22.04 LTS 2027
                        SourceSetup("Ubuntu", "jammy", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]]),
                        SourceSetup("Ubuntu", "jammy-security", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: jammy\nSuite: jammy-security"),
                        SourceSetup("Ubuntu", "jammy-backports", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: jammy\nSuite: jammy-backports"),
                    ],
                    "extra_options": "Deb-Build-Options: noddebs\n",
                },
                "impish": {
                    "arch_optional": ["i386"],
                    "sources": [  # impish: 21.10
                        SourceSetup("Ubuntu", "impish", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]]),
                        SourceSetup("Ubuntu", "impish-security", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: impish\nSuite: impish-security"),
                        SourceSetup("Ubuntu", "impish-backports", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: impish\nSuite: impish-backports"),
                    ],
                    "extra_options": "Deb-Build-Options: noddebs\n",
                },
                "hirsute": {
                    "arch_optional": ["i386"],
                    "sources": [  # hirsute: 21.04
                        SourceSetup("Ubuntu", "hirsute", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]]),
                        SourceSetup("Ubuntu", "hirsute-security", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: hirsute\nSuite: hirsute-security"),
                        SourceSetup("Ubuntu", "hirsute-backports", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: hirsute\nSuite: hirsute-backports"),
                    ],
                    "extra_options": "Deb-Build-Options: noddebs\n",
                },
                "groovy": {
                    "arch_optional": ["i386"],
                    "sources": [  # groovy: 20.10
                        SourceSetup("Ubuntu", "groovy", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]]),
                        SourceSetup("Ubuntu", "groovy-security", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: groovy\nSuite: groovy-security"),
                        SourceSetup("Ubuntu", "groovy-backports", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: groovy\nSuite: groovy-backports"),
                    ],
                    "extra_options": "Deb-Build-Options: noddebs\n",
                },
                "focal": {
                    # Ubuntu drops i386 (starting with focal)
                    # 'focal' still has i386 repo (debootrap still works), so i386 is still picked
                    # by mini-buildd's setup. However, builds no longer work (default resolver
                    # aptitude missing).
                    # Choosing "apt" resolver would master this hurdle, but this would also change
                    # the default resolver for amd64. Imho best solution for mini-buildd's setup
                    # is to make this arch optional.
                    # Ref: https://discourse.ubuntu.com/t/community-process-for-32-bit-compatibility/12598?u=d0od
                    "arch_optional": ["i386"],

                    # For focal, "warnfail" just does not work (see above).
                    "lintian_options": {
                        "warnfail": [],
                    },

                    "sources": [  # focal: 20.04 LTS 2025 ESM 2030
                        SourceSetup("Ubuntu", "focal", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]]),
                        SourceSetup("Ubuntu", "focal-security", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: focal\nSuite: focal-security"),
                        SourceSetup("Ubuntu", "focal-backports", [APT_KEYS["Ubuntu"]["2012"], APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: focal\nSuite: focal-backports"),
                    ],
                    "extra_options": "Deb-Build-Options: noddebs\n",
                },
                "bionic": {
                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },

                    "sources": [  # bionic: 18.04 LTS 2023 ESM 2028
                        SourceSetup("Ubuntu", "bionic", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]]),
                        SourceSetup("Ubuntu", "bionic-security", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: bionic\nSuite: bionic-security"),
                        SourceSetup("Ubuntu", "bionic-backports", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: bionic\nSuite: bionic-backports"),
                    ],
                    # .. error:: **compat** (:debbug:`730572`): ``ddeb`` reprepro workaround
                    # Starting with cosmic, Ubuntu uses 'ddeb' as file appendix for automated debug packages.
                    # reprepro can't handle these yet -- so this is needed for a workaround for the wizard setup.
                    "extra_options": "Deb-Build-Options: noddebs\n",
                },
                "xenial": {
                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },
                    "sources": [  # xenial: 16.04 LTS 2021 ESM 2026
                        SourceSetup("Ubuntu", "xenial", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]]),
                        SourceSetup("Ubuntu", "xenial-security", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: xenial\nSuite: xenial-security"),
                        SourceSetup("Ubuntu", "xenial-backports", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: xenial\nSuite: xenial-backports"),
                    ],
                },
                "trusty": {
                    "broken": "apt update works, package builds fail",

                    "lintian_options": {
                        "warnfail": ["--fail-on-warnings"],
                    },
                    "sources": [  # trusty: 14.04 LTS 2019 ESM 2024
                        SourceSetup("Ubuntu", "trusty", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]]),
                        SourceSetup("Ubuntu", "trusty-security", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: trusty\nSuite: trusty-security"),
                        SourceSetup("Ubuntu", "trusty-backports", [APT_KEYS["Ubuntu"]["current"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: trusty\nSuite: trusty-backports"),
                    ],
                    "extra_options": SETUP_BLOCKS.line("apt-binary-keys", top=True),
                },
            },
        },
    },
    "layout": {
        "Default": {
            "suites": SUITE_SETUP,
            "with_rollbacks": True,
        },
        "Default (no rollbacks)": {
            "suites": SUITE_SETUP,
            "with_rollbacks": False,
        },
        "Debian Developer": {
            "suites": {s: c for s, c in SUITE_SETUP.items() if s in ["stable", "testing", "unstable", "experimental"]},
            "with_rollbacks": False,
            "options": {
                "mandatory_version_regex": ".*",
                "experimental_mandatory_version_regex": ".*",
                "extra_options": "Meta-Distributions: unstable=sid-unstable experimental=sid-experimental\n",
            }
        },
    },
    "repository": {
        "default": {
            "layout": "Default",
            "distribution_filter": {
                "base_source__codename__regex": ".*",
            },
        },
        "test": {
            "layout": "Default",
            "distribution_filter": {
                "base_source__codename__regex": ".*",
            },
            "options": {
                "allow_unauthenticated_uploads": True,
            },
        },
        "debdev": {
            "layout": "Debian Developer",
            "distribution_filter": {
                "base_source__codename": "sid",
            },
            "options": {
                "extra_uploader_keyrings": ("# Allow Debian maintainers (must install the 'debian-keyring' package)\n"
                                            "/usr/share/keyrings/debian-keyring.gpg\n"),
            },
        },
    },
}


class Codenames(dict):
    def __init__(self):
        super().__init__()
        for vendor_setup in SETUP["vendor"].values():
            for codename, setup in vendor_setup["codename"].items():
                self[codename] = setup

    def get_supported(self, vendor):
        di = SETUP["vendor"][vendor]["distro_info"]
        return [codename for codename in di.supported() if codename in self]

    def get_supported_plus_lts(self, vendor):
        di = SETUP["vendor"][vendor]["distro_info"]
        return [codename for codename in di.supported() + di.mbd_lts() if codename in self]

    @classmethod
    def get_working(cls, vendor):
        return [codename for codename, setup in SETUP["vendor"][vendor]["codename"].items() if setup.get("broken") is None]

    @classmethod
    def is_security(cls, origin, codename):
        vendor_setup = SETUP["vendor"].get(origin)
        return vendor_setup is not None and vendor_setup["security_codename_regex"].match(codename)

    def get_lintian_options(self, codename, kind):
        """
        Get lintian options from SETUP.
        """
        try:
            return self[codename]["lintian_options"][kind]
        except KeyError as e:
            LOG.info(f"lintian options for {codename}/{kind}: Using defaults (key: {e})")
            return SETUP["lintian_options"][kind]


CODENAMES = Codenames()


class SbuildCheck():
    """
    Generic support for sbuild checks (lintian, piuparts, autopkgtest).

    >>> SbuildCheck("lindian", "disabled")
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: HTTP 400: Unknown sbuild checker: lindian (valid options: lintian,piuparts,autopkgtest)
    >>> SbuildCheck("lintian", "warnfall")
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: HTTP 400: Unknown sbuild check mode: warnfall (valid options: DISABLED,IGNORE,ERRFAIL,WARNFAIL)
    >>> sc = SbuildCheck("lintian", "warnfail")
    >>> sc.checker
    'lintian'
    >>> sc.mode
    <Mode.WARNFAIL: 3>
    """

    class Mode(enum.Enum):
        DISABLED = 0
        IGNORE = 1
        ERRFAIL = 2
        WARNFAIL = 3

        def __str__(self):
            return {
                self.DISABLED: "Don't run check",
                self.IGNORE: "Run check but ignore results",
                self.ERRFAIL: "Run check and fail on errors",
                self.WARNFAIL: "Run check and fail on warnings",
            }[self]

    CHECKERS = ["lintian", "piuparts", "autopkgtest"]
    CHOICES = [(mode.value, mode.name) for mode in Mode]

    #: From sbuild source code: We may expect these textual statuses:
    STATUSES_PASS = ["pass", "info"]
    STATUSES_WARN = ["warn", "no tests"]
    STATUSES_FAIL = ["error", "fail"]

    def __init__(self, checker, mode):
        if checker not in self.CHECKERS:
            raise mini_buildd.HTTPBadRequest(f"Unknown sbuild checker: {checker} (valid options: {','.join(self.CHECKERS)})")
        self.checker = checker
        try:
            self.mode = self.Mode[mode.upper() if isinstance(mode, str) else mode]
        except KeyError as e:
            raise mini_buildd.HTTPBadRequest(f"Unknown sbuild check mode: {mode} (valid options: {','.join([mode.name for mode in self.Mode])})") from e

    @classmethod
    def desc(cls):
        return "Mode to control if a check should prevent package installation (for non-experimental suites)."

    @classmethod
    def usage(cls):
        return ", ".join([f"'{mode.name}' ({mode})" for mode in cls.Mode])

    def check(self, status, ignore=False):
        """Check if status is ok in this mode."""
        return \
            ignore or \
            status in self.STATUSES_PASS or \
            self.mode in [self.Mode.DISABLED, self.Mode.IGNORE] or \
            (status in self.STATUSES_WARN and self.mode is not self.Mode.WARNFAIL)

    def lintian_options(self, codename):
        options = CODENAMES.get_lintian_options(codename, "common")
        if self.mode == self.Mode.WARNFAIL:
            options += CODENAMES.get_lintian_options(codename, "warnfail")
        return mini_buildd.PyCompat.shlex_join(options)


class Dist():
    """
    A mini-buildd distribution string.

    Normal distribution:

    >>> d = Dist("squeeze-test-stable")
    >>> d.codename, d.repository, d.suite
    ('squeeze', 'test', 'stable')
    >>> d.get()
    'squeeze-test-stable'

    Rollback distribution:

    >>> d = Dist("squeeze-test-stable-rollback5")
    >>> d.is_rollback
    True
    >>> d.get(rollback=False)
    'squeeze-test-stable'
    >>> d.codename, d.repository, d.suite, d.rollback
    ('squeeze', 'test', 'stable', 'rollback5')
    >>> d.get()
    'squeeze-test-stable-rollback5'
    >>> d.rollback_no
    5

    Malformed distributions:

    >>> Dist("-squeeze-stable")
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: HTTP 400: Malformed distribution '-squeeze-stable': Must be '<codename>-<repoid>-<suite>[-rollback<n>]'

    >>> Dist("squeeze--stable")
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: HTTP 400: Malformed distribution 'squeeze--stable': Must be '<codename>-<repoid>-<suite>[-rollback<n>]'

    >>> Dist("squeeze-test-stable-")
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: HTTP 400: Malformed distribution 'squeeze-test-stable-': Must be '<codename>-<repoid>-<suite>[-rollback<n>]'

    >>> Dist("squeeze-test-stable-rollback")
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: HTTP 400: Malformed distribution 'squeeze-test-stable-rollback': Must be '<codename>-<repoid>-<suite>[-rollback<n>]'

    >>> Dist("squeeze-test-stable-rolback0")
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: HTTP 400: Malformed distribution 'squeeze-test-stable-rolback0': Must be '<codename>-<repoid>-<suite>[-rollback<n>]'
    """

    _REGEX = re.compile(r"^\w+-\w+-\w+?(-rollback\d+)?$")

    def __init__(self, diststr):
        if not self._REGEX.match(diststr):
            raise mini_buildd.HTTPBadRequest(f"Malformed distribution '{diststr}': Must be '<codename>-<repoid>-<suite>[-rollback<n>]'")

        self.diststr = diststr
        self._dsplit = self.diststr.split("-")
        self.codename = self._dsplit[0]
        self.repository = self._dsplit[1]
        self.suite = self._dsplit[2]
        self.is_rollback = len(self._dsplit) == 4
        self.rollback = self._dsplit[3] if self.is_rollback else None
        self.rollback_no = int(re.sub(r"\D", "", self.rollback)) if self.rollback else None

    def get(self, rollback=True):
        return "-".join(self._dsplit) if rollback else "-".join(self._dsplit[:3])


def guess_codeversion(release):
    """
    Guess the 'codeversion'.

    Aka the first two digits of a Debian release version; for releases
    without version, this falls back to the uppercase codename.

    In Debian,
      - point release <= sarge had the 'M.PrN' syntax (with 3.1 being a major release).
      - point release in squeeze used 'M.0.N' syntax.
      - point releases for >= wheezy have the 'M.N' syntax (with 7.1 being a point release).
      - testing and unstable do not gave a version in Release and fall back to uppercase codename

    Ubuntu just uses YY.MM which we can use as-is.

    >>> guess_codeversion({"Origin": "Debian", "Version": "3.1r8", "Codename": "sarge"})
    '31'
    >>> guess_codeversion({"Origin": "Debian", "Version": "4.0r9", "Codename": "etch"})
    '40'
    >>> guess_codeversion({"Origin": "Debian", "Version": "6.0.6", "Codename": "squeeze"})
    '60'
    >>> guess_codeversion({"Origin": "Debian", "Version": "7.0", "Codename": "wheezy"})
    '7'
    >>> guess_codeversion({"Origin": "Debian", "Version": "7.1", "Codename": "wheezy"})
    '7'
    >>> guess_codeversion({"Origin": "Debian", "Codename": "jessie"})
    '~JESSIE'
    >>> guess_codeversion({"Origin": "Debian", "Codename": "sid"})
    '~SID'
    >>> guess_codeversion({"Origin": "Ubuntu", "Version": "12.10", "Codename": "quantal"})
    '1210'

    """
    try:
        ver_split = release["Version"].split(".")
        number0 = ver_split[0]
        number1 = ver_split[1].partition("r")[0]  # Some older Debian versions had an "r" in the number
        if release.get("Origin", None) == "Debian" and int(number0) >= 7:
            return number0  # Debian >= wheezy: '~testN': One number tells the codeversion
        return number0 + number1  # Debian < wheezy, Ubuntu, maybe others: 1st plus 2nd number is needed
    except BaseException as e:
        LOG.info(f"Falling back to codename for codeversion due to: {e}")
        return "~" + release["Codename"].upper()
