import logging
import functools

import django.core.exceptions
import django.db
import django.utils.html

import mini_buildd.dist
import mini_buildd.files
import mini_buildd.sbuild

import mini_buildd.models.base
import mini_buildd.models.source

LOG = logging.getLogger(__name__)


class Suite(mini_buildd.models.base.Model):
    name = django.db.models.CharField(
        max_length=100,
        help_text="A suite to support, usually s.th. like 'experimental', 'unstable', 'testing' or 'stable'.")

    def __str__(self):
        return f"{self.name}"

    def clean(self, *args, **kwargs):
        super().clean(*args, **kwargs)
        self.mbd_validate_regex(r"^[a-z]+$", self.name, "name")


class SuiteOption(mini_buildd.models.base.Model):
    layout = django.db.models.ForeignKey("Layout",
                                         on_delete=django.db.models.CASCADE)
    suite = django.db.models.ForeignKey("Suite",
                                        on_delete=django.db.models.CASCADE)

    uploadable = django.db.models.BooleanField(
        default=True,
        help_text="Whether this suite should accept user uploads.")

    experimental = django.db.models.BooleanField(
        default=False,
        help_text="""
Experimental suites must be uploadable and must not
migrate. Also, the packager treats failing extra QA checks (like
lintian) as non-lethal, and will install anyway.
""")

    migrates_to = django.db.models.ForeignKey(
        "self", blank=True, null=True,
        on_delete=django.db.models.CASCADE,
        help_text="Give another suite where packages may migrate to (you may need to save this 'blank' first before you see choices here).")

    build_keyring_package = django.db.models.BooleanField(
        default=False,
        help_text="Build keyring package for this suite (i.e., when the resp. Repository action is called).")

    auto_migrate_after = django.db.models.IntegerField(
        default=0,
        help_text="For future use. Automatically migrate packages after x days.")

    not_automatic = django.db.models.BooleanField(
        default=True,
        help_text="Include 'NotAutomatic' in the Release file.")

    but_automatic_upgrades = django.db.models.BooleanField(
        default=True,
        help_text="Include 'ButAutomaticUpgrades' in the Release file.")

    class Meta(mini_buildd.models.base.Model.Meta):
        unique_together = ("suite", "layout")

    def __str__(self):
        return self.suite.name

    def clean(self, *args, **kwargs):
        super().clean(*args, **kwargs)
        if self.build_keyring_package and not self.uploadable:
            raise django.core.exceptions.ValidationError("You can only build keyring packages on uploadable suites!")
        if self.experimental and self.migrates_to:
            raise django.core.exceptions.ValidationError("Experimental suites may not migrate!")
        if self.experimental and not self.uploadable:
            raise django.core.exceptions.ValidationError("Experimental suites must be uploadable!")
        if self.migrates_to and self.migrates_to.layout != self.layout:
            raise django.core.exceptions.ValidationError("Migrating suites must be in the same layout (you need to save once to make new suites visible).")
        if self.migrates_to and self.migrates_to.uploadable:
            raise django.core.exceptions.ValidationError("You may not migrate to an uploadable suite.")

    @property
    def rollback(self):
        """Rollback field temporarily implemented as extra_option."""
        return int(self.mbd_get_extra_option("Rollback", "0"))

    def mbd_get_sort_no(self):
        """Compute number that may be used to sort suites from 'stable' (0) towards 'experimental'."""
        no = 0
        if self.uploadable:
            no += 5
        if self.migrates_to:
            no += 5
        if self.experimental:
            no += 20
        return no


class SuiteOptionInline(django.contrib.admin.TabularInline):
    model = SuiteOption
    extra = 1
    exclude = ("pickled_data", "extra_options",)


class Layout(mini_buildd.models.base.Model):
    MBD_HELP_EXTRA_OPTIONS = """
Meta-Distributions: META=CODENAME-SUITE[ META=CODENAME-SUITE[...: Support METAs alone as distribution identifier.

Meta distribution identifiers should be unique across all
repositories; usually, a layout with meta distributions should
only be used by at most one repository.

Example:
---
Meta-Distributions: unstable=sid-unstable experimental=sid-experimental
---

(See standard layout 'Debian Developer'): allows upload/testing of
packages (to unstable,experimental,..) aimed for Debian.
"""

    name = django.db.models.CharField(primary_key=True, max_length=100)

    suites = django.db.models.ManyToManyField(Suite, through=SuiteOption)

    # Version magic
    default_version = django.db.models.CharField(
        max_length=100, default="~%IDENTITY%%CODEVERSION%+1",
        help_text=("""\
Version string to append to the original version for automated ports; you may use these placeholders:

%IDENTITY%: Repository identity (see 'Repository').
%CODEVERSION%: Numerical base distribution version (see 'Source').
"""))

    mandatory_version_regex = django.db.models.CharField(
        max_length=100, default=r"~%IDENTITY%%CODEVERSION%\+[1-9]",
        help_text="Mandatory version regex; you may use the same placeholders as for 'default version'.")

    # Version magic (experimental)
    experimental_default_version = django.db.models.CharField(
        max_length=30, default="~%IDENTITY%%CODEVERSION%+0",
        help_text="Like 'default version', but for suites flagged 'experimental'.")

    experimental_mandatory_version_regex = django.db.models.CharField(
        max_length=100, default=r"~%IDENTITY%%CODEVERSION%\+0",
        help_text="Like 'mandatory version', but for suites flagged 'experimental'.")

    class Admin(mini_buildd.models.base.Model.Admin):
        inlines = (SuiteOptionInline,)

    def __str__(self):
        return f"{self.name}"

    def mbd_get_reverse_dependencies(self):
        """When the layout changes, all repos that use that layout also change."""
        return list(self.repository_set.all())


class ArchitectureOption(mini_buildd.models.base.Model):
    architecture = django.db.models.ForeignKey("Architecture",
                                               on_delete=django.db.models.CASCADE)
    distribution = django.db.models.ForeignKey("Distribution",
                                               on_delete=django.db.models.CASCADE)

    optional = django.db.models.BooleanField(
        default=False,
        help_text="Don't care if the architecture can't be build.")

    build_architecture_all = django.db.models.BooleanField(
        default=False,
        help_text="Use to build arch-all packages.")

    class Meta(mini_buildd.models.base.Model.Meta):
        unique_together = ("architecture", "distribution")

    def __str__(self):
        return f"{self.architecture} for {self.distribution}"

    def clean(self, *args, **kwargs):
        super().clean(*args, **kwargs)
        if self.build_architecture_all and self.optional:
            raise django.core.exceptions.ValidationError("Optional architectures must not be architecture all!")


class ArchitectureOptionInline(django.contrib.admin.TabularInline):
    model = ArchitectureOption
    exclude = ("pickled_data", "extra_options",)
    extra = 1


@functools.total_ordering
class Distribution(mini_buildd.models.base.Model):
    MBD_HELP_EXTRA_OPTIONS = f"""
===============================================================================================
Sbuild-Config-Blocks[-Top]: <BLOCK> ...: Use predefined sbuild config blocks (space-separated).
===============================================================================================
{mini_buildd.sbuild.CONFIG_BLOCKS.usage()}

===============================================================================================
Sbuild-Setup-Blocks[-Top]: <BLOCK> ...: Use predefined sbuild setup blocks (space-separated).
===============================================================================================
{mini_buildd.sbuild.SETUP_BLOCKS.usage()}

===============================================================================================
Autopkgtest-Mode: <MODE>: {mini_buildd.dist.SbuildCheck.desc()}
===============================================================================================
{mini_buildd.dist.SbuildCheck.usage()}

===============================================================================================
Internal-APT-Priority: <PRIORITY>: Set APT priority for internal apt sources in builds.
===============================================================================================
The default is 1, which means you will only build against newer
packages in our own repositories in case it's really needed by the
package's build dependencies. This is the recommended behaviour,
producing sparse dependencies.

However, some packages with incorrect build dependencies might
break anyway, while they would work fine when just build against
the newest version available.

You may still solve this on a per-package basis, using the
resp. upload option via changelog. However, in case you don't care
about sparse dependencies in this distribution in general, you can
pimp the internal priority up here.

Example: Always build against newer internal packages:
---
Internal-APT-Priority: 500
---
===============================================================================================
Deb-Build-Options: ...: Set extra build options.
===============================================================================================
Values to add to environment variable ``DEB_BUILD_OPTIONS`` used when building packages. See

* ``https://www.debian.org/doc/debian-policy/ch-source.html#debian-rules-and-deb-build-options`` and
* ``https://lists.debian.org/debian-devel/2015/12/msg00262.html'>Debian dbgsym announcement``

for valid options. Value may be overridden by the resp. user upload option.

Option ``noddebs`` is useful for Ubuntu distributions that for
some reason create their automated debug packages with file appendix
'ddeb', as (see ``https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=730572``) reprepo fails on them.

Example: Never build automatic debug packages (Ubuntu bionic, cosmic):
---
Deb-Build-Options: noddebs
---
===============================================================================================
Deb-Build-Profiles: ...: Set extra build profiles.
===============================================================================================
Contents of DEB_BUILD_PROFILES environment for building (see ``https://wiki.debian.org/BuildProfileSpec``).
Value may be overridden by the resp. user upload option.
"""

    base_source = django.db.models.ForeignKey(mini_buildd.models.source.Source,
                                              on_delete=django.db.models.CASCADE)
    extra_sources = django.db.models.ManyToManyField(mini_buildd.models.source.PrioritySource, blank=True)
    components = django.db.models.ManyToManyField(mini_buildd.models.source.Component)

    architectures = django.db.models.ManyToManyField(mini_buildd.models.source.Architecture, through=ArchitectureOption)

    RESOLVER_APT = 0
    RESOLVER_APTITUDE = 1
    RESOLVER_INTERNAL = 2
    RESOLVER_CHOICES = (
        (RESOLVER_APT, "apt"),
        (RESOLVER_APTITUDE, "aptitude"),
        (RESOLVER_INTERNAL, "internal"))
    build_dep_resolver = django.db.models.IntegerField(choices=RESOLVER_CHOICES, default=RESOLVER_APTITUDE)

    apt_allow_unauthenticated = django.db.models.BooleanField(default=False)

    lintian_mode = django.db.models.IntegerField(choices=mini_buildd.dist.SbuildCheck.CHOICES, default=mini_buildd.dist.SbuildCheck.Mode.ERRFAIL.value,
                                                 help_text=(f"""\
{mini_buildd.dist.SbuildCheck.desc()}

{mini_buildd.dist.SbuildCheck.usage()}
"""))

    lintian_extra_options = django.db.models.CharField(max_length=200, default="--info")

    # piuparts not used atm; placeholder for later
    piuparts_mode = django.db.models.IntegerField(choices=mini_buildd.dist.SbuildCheck.CHOICES, default=mini_buildd.dist.SbuildCheck.Mode.DISABLED.value,
                                                  help_text=(f"""\
EXPERIMENTAL: {mini_buildd.dist.SbuildCheck.desc()}

{mini_buildd.dist.SbuildCheck.usage()}

EXPERIMENTAL: All modes but 'Run check and ignore errors' are currently ignored, and even that might not be called correctly.
"""))
    piuparts_extra_options = django.db.models.CharField(max_length=200, default="--info")
    piuparts_root_arg = django.db.models.CharField(max_length=200, default="sudo")

    chroot_setup_script = django.db.models.TextField(blank=True,
                                                     default="",
                                                     help_text="""\
Custom script that will be run via sbuild's ``--chroot-setup-command``. Be sure to provide a proper shebang like ``#!/bin/sh -e``.

Note that some predefined script blocks to choose from are available (see 'Extra Options'), which might suite your needs already.

Example:
---
#!/bin/sh -e

echo "I: Foo bar..."
---
""")
    sbuildrc_snippet = django.db.models.TextField(blank=True,
                                                  default="",
                                                  help_text="""\
Custom perl snippet to be added to ``.sbuildrc`` for each build.

Note that some predefined config blocks to choose from are available (see 'Extra Options'), which might suite your needs already.

You may use these placeholders:

* ``%LIBDIR%``: Per-chroot persistent dir; may be used for data that should persist between builds (caches, ...).

Example:
---
$build_environment = { 'MY_FOO_BAR' => '%LIBDIR%/my-foo-bar' };
---
""")

    class Meta(mini_buildd.models.base.StatusModel.Meta):
        ordering = ["base_source"]

    class Admin(mini_buildd.models.base.Model.Admin):
        inlines = (ArchitectureOptionInline,)
        filter_horizontal = ("extra_sources", "components",)

    def mbd_is_active(self):
        for s in [self.base_source] + [s.source for s in self.extra_sources.all()]:
            if not s.mbd_is_active():
                return False
        return True

    def __str__(self):
        return f"{self.base_source}: {' '.join([a.architecture.name for a in self.architectureoption_set.all()])}"

    def mbd_get_components(self):
        return [c.name for c in sorted(self.components.all())]

    def mbd_get_archall_architectures(self):
        return [a.architecture.name for a in self.architectureoption_set.all() if a.build_architecture_all]

    def mbd_get_mandatory_architectures(self):
        return [a.architecture.name for a in self.architectureoption_set.all() if not a.optional]

    def mbd_get_reverse_dependencies(self):
        """When the distribution changes, all repos that use that distribution also change."""
        return list(self.repository_set.all())

    __hash__ = mini_buildd.models.base.Model.__hash__

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.base_source == other.base_source
        return NotImplemented

    def __lt__(self, other):
        if isinstance(other, self.__class__):
            return self.base_source < other.base_source
        return NotImplemented

    def clean(self, *args, **kwargs):
        super().clean(*args, **kwargs)
        if self.chroot_setup_script:
            try:
                mini_buildd.files.File.shebang_from_data(self.chroot_setup_script)
            except BaseException as e:
                raise django.core.exceptions.ValidationError({"chroot_setup_script": "Missing shebang"}) from e

        try:
            mini_buildd.sbuild.CONFIG_BLOCKS.validate_all(get=self.mbd_get_extra_option)
            mini_buildd.sbuild.SETUP_BLOCKS.validate_all(get=self.mbd_get_extra_option)
        except mini_buildd.HTTPError as e:
            raise django.core.exceptions.ValidationError({"extra_options": f"{e}"})
