"""
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 functools
import abc

import distro_info

import debian.debian_support

import mini_buildd
from mini_buildd.sbuild import SETUP_BLOCKS

LOG = logging.getLogger(__name__)


class Archs:
    """Deliver lists of Debian architectures the current system can handle"""

    _NATIVE_ARCHS_MAP = {"amd64": ["i386"]}

    @classmethod
    def _atwarn(cls, e):
        LOG.warning(f"Consider to install ``arch-test`` to avoid: {e}")

    @classmethod
    def native(cls):
        try:
            return mini_buildd.call.Call(["arch-test", "-n"]).check().stdout.split()
        except Exception as e:
            cls._atwarn(e)
            native = mini_buildd.call.Call(["dpkg", "--print-architecture"]).check().stdout.strip()
            return [native] + cls._NATIVE_ARCHS_MAP.get(native, [])

    @classmethod
    def available(cls):
        try:
            return mini_buildd.call.Call(["arch-test"]).check().stdout.split()
        except Exception as e:
            cls._atwarn(e)
            return cls.native()


class DistroInfo(distro_info.DistroInfo):
    def mbd_origin(self):
        return self._distro

    def mbd_release(self, codename):
        for r in self._releases:
            if r.series == codename:
                return r
        return None

    def mbd_cmpversion(self, codename, codeversion_):  # pylint: disable=unused-argument
        return codeversion_

    @abc.abstractmethod
    def mbd_lts(self):
        pass


class DebianDistroInfo(DistroInfo, 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()

    def mbd_cmpversion(self, codename, codeversion_):
        epoch = ""
        if self.devel() == codename:
            epoch = "2:"
        if self.testing() == codename:
            epoch = "1:"
        return epoch + codeversion_


class UbuntuDistroInfo(DistroInfo, 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


#: Available distro infos
DISTRO_INFO = {
    "Debian": DebianDistroInfo(),
    "Ubuntu": UbuntuDistroInfo(),
}


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": {
        "2018": "871920D1991BC93C",   # Ubuntu Archive Automatic Signing Key (2018) <ftpmaster@ubuntu.com>
        "2012": "3B4FE6ACC0B21F32",   # Ubuntu Archive Automatic Signing Key (2012) <ftpmaster@ubuntu.com>
        "2004": "40976EAF437D05B5",   # Ubuntu Archive Automatic Signing Key <ftpmaster@ubuntu.com>
    },
    "PureOS": {
        "repository": "2B4A53F2B41CE072",   # Purism PureOS Archive (PureOS Archive Signing Key) <sysadmin@puri.sm>
    },
}

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"],
    },

    "origin": {
        "Debian": {
            #: "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"]]),
                    ],
                },
                "bookworm": {
                    "sources": [
                        SourceSetup("Debian", "bookworm", [APT_KEYS["Debian"]["buster"]["archive"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                        SourceSetup("Debian", "bookworm-security", [APT_KEYS["Debian"]["buster"]["security"], APT_KEYS["Debian"]["bullseye"]["security"]],
                                    "X-Remove-From-Component: updates/"),
                    ],
                },
                "bullseye": {
                    "sources": [
                        SourceSetup("Debian", "bullseye", [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"]["buster"]["archive"], APT_KEYS["Debian"]["buster"]["release"], APT_KEYS["Debian"]["bullseye"]["archive"]]),
                        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"]["stretch"]["security"], APT_KEYS["Debian"]["buster"]["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"]["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"]["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-key-add apt-no-check-valid-until apt-urold", 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": {
            "archive_paths": ["ubuntu", "ubuntu-old"],
            "archive": [
                "http://archive.ubuntu.com/ubuntu/",       # Releases
                "http://security.ubuntu.com/ubuntu/",      # Security
                "http://old-releases.ubuntu.com/ubuntu/",  # Older Releases
            ],
            "default_components": ["main", "universe", "restricted", "multiverse"],

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

            "codename": {
                "kinetic": {
                    "arch_optional": ["i386"],
                    "sources": [  # kinetic: 22.10
                        SourceSetup("Ubuntu", "kinetic", [APT_KEYS["Ubuntu"]["2018"]]),
                        SourceSetup("Ubuntu", "kinetic-security", [APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: kinetic\nSuite: kinetic-security"),
                        SourceSetup("Ubuntu", "kinetic-backports", [APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: kinetic\nSuite: kinetic-backports"),
                    ],
                    "extra_options": "Deb-Build-Options: noddebs\n",
                },
                "jammy": {
                    "arch_optional": ["i386"],
                    "sources": [  # jammy: 22.04 LTS 2027
                        SourceSetup("Ubuntu", "jammy", [APT_KEYS["Ubuntu"]["2018"]]),
                        SourceSetup("Ubuntu", "jammy-security", [APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: jammy\nSuite: jammy-security"),
                        SourceSetup("Ubuntu", "jammy-backports", [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"]["2018"]]),
                        SourceSetup("Ubuntu", "impish-security", [APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: impish\nSuite: impish-security"),
                        SourceSetup("Ubuntu", "impish-backports", [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"]["2018"]]),
                        SourceSetup("Ubuntu", "hirsute-security", [APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: hirsute\nSuite: hirsute-security"),
                        SourceSetup("Ubuntu", "hirsute-backports", [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"]["2018"]]),
                        SourceSetup("Ubuntu", "groovy-security", [APT_KEYS["Ubuntu"]["2018"]],
                                    "Codename: groovy\nSuite: groovy-security"),
                        SourceSetup("Ubuntu", "groovy-backports", [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"]["2012"]]),
                        SourceSetup("Ubuntu", "bionic-security", [APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: bionic\nSuite: bionic-security"),
                        SourceSetup("Ubuntu", "bionic-backports", [APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: bionic\nSuite: bionic-backports"),
                    ],
                    # .. attention:: **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"]["2004"], APT_KEYS["Ubuntu"]["2012"]]),
                        SourceSetup("Ubuntu", "xenial-security", [APT_KEYS["Ubuntu"]["2004"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: xenial\nSuite: xenial-security"),
                        SourceSetup("Ubuntu", "xenial-backports", [APT_KEYS["Ubuntu"]["2004"], 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"]["2004"], APT_KEYS["Ubuntu"]["2012"]]),
                        SourceSetup("Ubuntu", "trusty-security", [APT_KEYS["Ubuntu"]["2004"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: trusty\nSuite: trusty-security"),
                        SourceSetup("Ubuntu", "trusty-backports", [APT_KEYS["Ubuntu"]["2004"], APT_KEYS["Ubuntu"]["2012"]],
                                    "Codename: trusty\nSuite: trusty-backports"),
                    ],
                    "extra_options": SETUP_BLOCKS.line("apt-binary-keys", top=True),
                },
            },
        },

        #: PureOS (https://pureos.net/)
        "PureOS": {
            "archive_paths": [],
            "archive": [
                "https://mirror.fsf.org/pureos/",
            ],
            "default_components": ["main"],

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

            "codename": {
                "amber": {
                    "sources": [  # amber
                        SourceSetup("PureOS", "amber", [APT_KEYS["PureOS"]["repository"]]),
                        SourceSetup("PureOS", "amber-security", [APT_KEYS["PureOS"]["repository"]]),
                        SourceSetup("PureOS", "amber-updates", [APT_KEYS["PureOS"]["repository"]]),
                    ],
                },
            },
        },
    },
    "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"),
            },
        },
    },
}


def guess_origin(codename):
    """
    Guess Origin from codename

    >>> guess_origin("rex")  # only via distro info
    'Debian'
    >>> guess_origin("warty")  # only via distro info
    'Ubuntu'
    >>> guess_origin("bullseye")  # via setup
    'Debian'
    """
    for origin, setup in SETUP["origin"].items():
        if codename in setup["codename"]:
            return origin

        # Check if we can find codename in distro info
        di = DISTRO_INFO.get(origin)
        if di and codename in di.all:
            return origin
    return None


#: List of Debian codenames using something ``3.1`` as main version (all <= squeeze)
DEBIAN_MAJOR_MINOR_CODENAMES = ["buzz", "rex", "bo", "hamm", "slink", "potato", "woody", "sarge", "etch", "lenny", "squeeze"]


def guess_codeversion(origin, codename, version):
    """
    Get recommended codeversion from origin/codename/version triple

    ``codename`` is essentially one number representing the release version, which can later be used as in
    mandatory version part, or for sorting codenames.

    ``version`` may be from a ``Release`` file (like '11.3' for bullseye), or from ``distro-info-data``
    (just '11'). If empty, ``~CODENAME`` is returned.

    Some heuristics (tailored for Debian and Ubuntu, but may work fine for other origins) are applied to
    produce a reasonable ``codeversion``.

    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("Debian", "sarge", "3.1r8")
    '31'
    >>> guess_codeversion("Debian", "etch", "4.0r9")
    '40'
    >>> guess_codeversion("Debian", "squeeze", "6.0.6")
    '60'
    >>> guess_codeversion("Debian", "wheezy", "7.0")
    '7'
    >>> guess_codeversion("Debian", "wheezy", "7.1")
    '7'
    >>> guess_codeversion("Ubuntu", "quantal", "12.10")
    '1210'
    """
    if not version:
        return "~" + codename.upper()

    ver_split = version.split(".")

    def number(no):
        """Be sure to not fail (return empty str instead), and only use *starting* digits"""
        v = ver_split[no] if len(ver_split) > no else ""
        m = re.search(r"^(\d+)", v)
        return m.group(1) if m else ""

    if origin == "Debian" and codename not in DEBIAN_MAJOR_MINOR_CODENAMES:
        return number(0)  # Debian >= wheezy
    return f"{number(0)}{number(1)}"


@functools.total_ordering
class Codename():
    def __init__(self, codename, origin="", version=""):
        self.codename = codename
        self.origin = origin if origin else guess_origin(codename)
        self.codeversion, self.cmpversion = "", ""

        di = DISTRO_INFO.get(self.origin)

        # Set or compute version (may be empty string)
        if not version and di is not None:
            r = di.mbd_release(codename)
            version = di.version(self.codename) if r.release else ""  # Version might be non-empty in distro-info-data even if not released

        # Compute code- and cmpversion
        self.codeversion = guess_codeversion(self.origin, self.codename, version)
        self.cmpversion = self.codeversion
        if di is not None:
            self.cmpversion = di.mbd_cmpversion(self.codename, self.codeversion)

    def __str__(self):
        return f"{self.origin} '{self.codename}' ({self.codeversion})"

    def __hash__(self):
        return hash(self.codename)

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.origin == other.origin and self.codename == other.codename
        return NotImplemented

    def __lt__(self, other):
        if isinstance(other, self.__class__):
            if self.origin == other.origin:
                return debian.debian_support.Version(self.cmpversion) < debian.debian_support.Version(other.cmpversion)
            return self.origin < other.origin
        return NotImplemented


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

    def get_supported(self, origin):
        di = DISTRO_INFO[origin]
        return [codename for codename in di.supported() if codename in self]

    def get_supported_plus_lts(self, origin):
        di = DISTRO_INFO[origin]
        return [codename for codename in di.supported() + di.mbd_lts() if codename in self]

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

    @classmethod
    def is_security(cls, origin, codename):
        origin_setup = SETUP["origin"].get(origin)
        return origin_setup is not None and origin_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.debug(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")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: Unknown sbuild checker: lindian (valid options: lintian,piuparts,autopkgtest) (HTTP 400 Bad request syntax or unsupported method)

    >>> SbuildCheck("lintian", "warnfall")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: Unknown sbuild check mode: warnfall (valid options: DISABLED,IGNORE,ERRFAIL,WARNFAIL) (HTTP 400 Bad request syntax or unsupported method)

    >>> 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")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: Malformed distribution '-squeeze-stable': Must be '<codename>-<repoid>-<suite>[-rollback<n>]' (HTTP 400 Bad request syntax or unsupported method)

    >>> Dist("squeeze--stable")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: Malformed distribution 'squeeze--stable': Must be '<codename>-<repoid>-<suite>[-rollback<n>]' (HTTP 400 Bad request syntax or unsupported method)

    >>> Dist("squeeze-test-stable-")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: Malformed distribution 'squeeze-test-stable-': Must be '<codename>-<repoid>-<suite>[-rollback<n>]' (HTTP 400 Bad request syntax or unsupported method)

    >>> Dist("squeeze-test-stable-rollback")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: Malformed distribution 'squeeze-test-stable-rollback': Must be '<codename>-<repoid>-<suite>[-rollback<n>]' (HTTP 400 Bad request syntax or unsupported method)

    >>> Dist("squeeze-test-stable-rolback0")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    mini_buildd.HTTPBadRequest: Malformed distribution 'squeeze-test-stable-rolback0': Must be '<codename>-<repoid>-<suite>[-rollback<n>]' (HTTP 400 Bad request syntax or unsupported method)
    """

    _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])


if __name__ == "__main__":
    for DI in [DebianDistroInfo(), UbuntuDistroInfo()]:
        for C in DI.all:
            print(Codename(C))
