import os
import re
import subprocess
import logging

import mini_buildd.files
import mini_buildd.call

LOG = logging.getLogger(__name__)


class Blocks(mini_buildd.files.Dir):
    def __init__(self, type_):
        super().__init__()
        self.type = type_

    def extra_option(self, top=False):
        return f"Sbuild-{self.type.capitalize()}-Blocks{'-Top' if top else ''}"

    def extra_options(self):
        return [self.extra_option(top=True), self.extra_option()]

    def usage(self):
        ml = max([len(name) for name in self.extra_options()])  # "max length" helper
        header = (f"{self.extra_option(top=True):<{ml}}: Blocks before the automated {self.type}.\n"
                  f"{self.extra_option(top=False):<{ml}}: Blocks after the automated {self.type}.\n\n"
                  "Available blocks:\n\n")
        ml = max([len(name) for name in self.keys()])
        return header + "\n".join([f"{name:<{ml}}: {block.description}" for name, block in self.items()])

    def validate(self, extra_option):
        """Validate extra option value from user space (string, space separated). Return extra option value as list."""
        eol = extra_option.split()  # extra option list
        unknown = set(eol) - set(self.keys())
        if unknown:
            raise mini_buildd.HTTPBadRequest(f"Unknown sbuild {self.type} block(s): {' '.join(unknown)} (check 'Extra Options' configurations in Distributions)")
        return eol

    def validate_all(self, get):
        for extra_option in self.extra_options():
            self.validate(get(extra_option, ""))


class ConfigBlocks(Blocks):
    def __init__(self):
        super().__init__("config")

    def default(self):
        return f"{self.extra_option(top=False)}: ccache\n"

    def configure(self, file_, get, top):
        """Add configured blocks to provided file (space separated config option is auto-magically received via provided getter)."""
        for block in self.validate(get(self.extra_option(top=top), "")):
            file_.add(self[block].get(snippet=True))


class SetupBlocks(Blocks):
    def __init__(self):
        super().__init__("setup")

    def default(self):
        return f"{self.extra_option(top=False)}: ccache eatmydata\n"

    def configure(self, dir_, get, top):
        """Add configured blocks to provided dir (space separated config option is auto-magically received via provided getter)."""
        dir_.add_dir(self, name_filter=self.validate(get(self.extra_option(top=top), "")))


#: Quiet, non-interactive, least invasive and loggable apt-get call (Dpkg::Use-Pty=false is to avoid https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=539617).
APT_GET = "apt-get --quiet --yes --option=APT::Install-Recommends=false --option=Acquire::Languages=none --option=Dpkg::Use-Pty=false --option=Dpkg::Options::=--force-confdef --option=Dpkg::Options::=--force-confnew"

CONFIG_BLOCKS = ConfigBlocks()

CONFIG_BLOCKS.add(mini_buildd.files.PerlModule("ccache",
                                               description="Enable ccache",
                                               snippet="""\
$path = "/usr/lib/ccache:$path";
$$build_environment{'CCACHE_DIR'} = '%LIBDIR%/.ccache';
"""))

SETUP_BLOCKS = SetupBlocks()

SETUP_BLOCKS.add(mini_buildd.files.ShScript("ccache",
                                            description="Install 'ccache'. Use together with 'ccache' config block to get a (per-instance) cache.",
                                            snippet=f"""\
{APT_GET} install ccache
"""))

SETUP_BLOCKS.add(mini_buildd.files.ShScript("eatmydata",
                                            description="Install 'eatmydata' when available.",
                                            snippet=f"""\
if {APT_GET} install eatmydata; then
    # eatmydata <= 26: Just one 'deb', and not multiarched
    EATMYDATA_SO=$(dpkg -L eatmydata | grep 'libeatmydata.so$') || true
    if [ -z "${{EATMYDATA_SO}}" ]; then
        # eatmydata >= 82: Has a library 'deb', and is multiarched
        EATMYDATA_SO=$(dpkg -L libeatmydata1 | grep 'libeatmydata.so$') || true
    fi
    if [ -n "${{EATMYDATA_SO}}" ]; then
        printf " ${{EATMYDATA_SO}}" >> /etc/ld.so.preload
    else
        printf "W: eatmydata: No *.so found (skipping)\\n" >&2
    fi
else
    printf "W: eatmydata: Not installable (skipping)\\n" >&2
fi
"""))

SETUP_BLOCKS.add(mini_buildd.files.BashScript("apt-disable-check-valid-until",
                                              description="Disable APT's 'Check-Valid-Until' globally per config. Only possibly needed <stretch, where this option can't be given as apt line option.",
                                              snippet="""\
printf "Acquire::Check-Valid-Until \\"false\\";\\n" >"/etc/apt/apt.conf.d/10disable-check-valid-until"
"""))

SETUP_BLOCKS.add(mini_buildd.files.ShScript("sun-java6-license",
                                            description="Accept sun-java6 licence (debconf) so we can build-depend on it (sun-java is no longer part of Debian since 2011).",
                                            snippet="""\
echo "sun-java6-bin shared/accepted-sun-dlj-v1-1 boolean true" | debconf-set-selections --verbose
echo "sun-java6-jdk shared/accepted-sun-dlj-v1-1 boolean true" | debconf-set-selections --verbose
echo "sun-java6-jre shared/accepted-sun-dlj-v1-1 boolean true" | debconf-set-selections --verbose
"""))

SETUP_BLOCKS.add(mini_buildd.files.ShScript("bash-devices",
                                            description="Fixup /dev/fd|stdin|stdout|stderr. Needed for building bash < 3.0-17 (2005) only. See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=327477.",
                                            snippet="""\
[ -e /dev/fd ] || ln -sv /proc/self/fd /dev/fd
[ -e /dev/stdin ] || ln -sv fd/0 /dev/stdin
[ -e /dev/stdout ] || ln -sv fd/1 /dev/stdout
[ -e /dev/stderr ] || ln -sv fd/2 /dev/stderr
"""))

SETUP_BLOCKS.add(mini_buildd.files.BashScript("schroot-shm",
                                              description="Fixup /dev/shm mount (needed when using schroot < 1.6.10-3 (2017) only). See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728096.",
                                              snippet="""\
# Pre 1.99.19, this used to live here: /etc/schroot/setup.d/15mini-buildd-workarounds

# '/dev/shm' might be a symlink to '/run/shm' in some
# chroots. Depending on the system mini-buildd/sbuild runs on
# and what chroot is being build for this may or may not lead to
# one or both of these problems:
#
# - shm not mounted properly in build chroot (leading to build errors when shm is used).
# - shm mount on *the host* gets overloaded (leading to an shm mount "leaking" on the host).
#
# This workaround
#
# - [in '/etc/schroot/mini-buildd/fstab-generic'] Removes shm form the generic sbuild mounts (avoids the mount leaking).
# - [here] Fixes /dev/shm to a directory in case it's not.
# - [here] Always mounts /dev/shm itself.
#
# Debian Bug: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728096
#
# When this is fixed (and we have added a versioned depends on
# schroot accordingly), all of the above may be reverted again.
#
printf "=> Fixing up /dev/shm mount (Bug #728096):\\n"

if [ -L "/dev/shm" ]; then
  printf "Removing /dev/shm symlink...\\n"
  rm -v "/dev/shm"
fi
mkdir -v -p "/dev/shm"
mount -v -ttmpfs none "/dev/shm"
"""))

SETUP_BLOCKS.add(mini_buildd.files.ShScript("apt-force-urold",
                                            description="Try some hard configs for urold releases (squeeze or older) to make APT work (experimental).",
                                            snippet="""\
rm -v -r -f /var/lib/apt/lists/*
APT_FIX="/etc/apt/apt.conf.d/mini-buildd-apt-fix-squeeze"
printf "%s\\n" "Acquire::http::No-Cache true;" "Acquire::http::Pipeline-Depth 0;" "APT::Get::Force-Yes true;" >"${APT_FIX}"
cat "${APT_FIX}"
"""))


class SBuild():
    def __init__(self, breq):
        self.breq = breq
        self.breq.untar(self.breq.builds_path.full)

        self.config_path = self.breq.builds_path.new_sub([".config"])
        self.sbuildrc_file_path = self.breq.builds_path.join(".sbuildrc")
        self.buildlog_file_path = self.breq.builds_path.join(self.breq.dfn.buildlog(self.breq["architecture"]))
        self.call = None

        #
        # Generate .sbuildrc
        #
        sbuildrc = mini_buildd.files.PerlModule(".sbuildrc",
                                                placeholders={"LIBDIR": mini_buildd.config.ROUTES["libdir"].path.new_sub([str(self.breq.dist.codename), self.breq["architecture"]], create=True).full})

        sbuildrc.add("$path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games';",
                     description="Always set $path, so config blocks may add to it (this is sbuild's default path)")
        CONFIG_BLOCKS.configure(sbuildrc, self.breq.cget, top=True)
        sbuildrc.add("$apt_update = 0;",
                     description="Disable sbuild's internal apt update (we do a custom update later via chroot-setup commands)")
        sbuildrc.add(f"$apt_allow_unauthenticated = {self.breq.cget('Apt-Allow-Unauthenticated')};",
                     description="Allow unauthenticated apt toggle (from buildrequest)")
        CONFIG_BLOCKS.configure(sbuildrc, self.breq.cget, top=False)
        sbuildrc.add_file(self.config_path.join("sbuildrc_snippet"),
                          description="Custom sbuild config (from Distribution)")
        sbuildrc.save_as(self.sbuildrc_file_path)

        #
        # Generate setup.d/
        #
        setup = mini_buildd.files.Dir(self.breq.builds_path.join(".setup.d"))
        sources_list_file = self.config_path.join("apt_sources.list")

        setup.add(mini_buildd.files.ShScript("auto-show-sbuildrc",
                                             description="Print used '.sbuildrc' (FYI)",
                                             snippet=f"cat '{self.sbuildrc_file_path}'"))
        if self.breq.cget('Apt-Allow-Unauthenticated', "0") == "1":
            setup.add(mini_buildd.files.ShScript("auto-apt-allow-unauthenticated",
                                                 description="Allow unauthenticated APT (as per Distribution.apt_allow_unauthenticated setting)",
                                                 snippet="printf 'APT::Get::AllowUnauthenticated \"true\";\\n' >'/etc/apt/apt.conf.d/10allow-unauthenticated'"))

        SETUP_BLOCKS.configure(setup, self.breq.cget, top=True)
        if mini_buildd.files.SourcesList.has_https(sources_list_file):
            setup.add(mini_buildd.files.ShScript("auto-apt-https",
                                                 description="Enable APT https support",
                                                 snippet=f"""\
{APT_GET} install ca-certificates apt-transport-https
cp -v {self.config_path.join('ssl_cert')} /usr/local/share/ca-certificates/mini-buildd-repo.crt
/usr/sbin/update-ca-certificates
"""))

        setup.add(mini_buildd.files.ShScript("auto-apt-keys",
                                             description="Setup APT keys from buildrequest",
                                             snippet=f"""\
__install_gnupg()
{{
  # Workaround: Very old dists still may need to use apt-key (squeeze or older).
  # Workaround: Old dists may need to convert .asc to .gpg (jessie).
  # Workaround: Just using [trusted=yes] (>=wheezy) may work to obsolete all that, but seems unwise.
  {APT_GET} update
  {APT_GET} install gnupg
}}

APT_TRUSTED_GPGD="/etc/apt/trusted.gpg.d"
if [ -d "${{APT_TRUSTED_GPGD}}" ]; then
  cp -v "{self.config_path.join('apt_keys')}" "${{APT_TRUSTED_GPGD}}/mini-buildd-buildrequest.asc"
  case ${{SCHROOT_CHROOT_NAME}} in
    *-etch-*|*-lenny-*|*-squeeze-*|*-wheezy-*|*-jessie-*)
        echo "W: ${{SCHROOT_CHROOT_NAME}}: [apt <1.4/stretch] Only supports key.gpg (not key.asc) in ${{APT_TRUSTED_GPGD}}." >&2
        __install_gnupg
        gpg --dearmor "${{APT_TRUSTED_GPGD}}/mini-buildd-buildrequest.asc"
      ;;
  esac
else
  echo "W: ${{SCHROOT_CHROOT_NAME}}: Only supports deprecated apt-key." >&2
  __install_gnupg
  apt-key add "{self.config_path.join('apt_keys')}"
fi
"""))

        setup.add(mini_buildd.files.ShScript("auto-apt-update",
                                             description="Setup APT sources and preferences, and update",
                                             snippet=f"""\
cp -v "{sources_list_file}" /etc/apt/sources.list
cat /etc/apt/sources.list

cp -v "{self.config_path.join('apt_preferences')}" /etc/apt/preferences
cat /etc/apt/preferences

{APT_GET} update
apt-cache policy
"""))

        SETUP_BLOCKS.configure(setup, self.breq.cget, top=False)
        custom_buildrequest = self.config_path.join("chroot_setup_script")
        if os.path.getsize(custom_buildrequest) > 0:
            setup.add(mini_buildd.files.AutoScript(custom_buildrequest))
        setup.save()

        #
        # Generate command line
        #
        self.cmdline = ["sbuild",
                        "--dist", self.breq["distribution"],
                        "--arch", self.breq["architecture"],
                        "--chroot", self.breq.schroot_name()]

        for name in setup.keys():
            script = os.path.join(setup.path, name)
            self.cmdline += ["--chroot-setup-command", script]

        self.cmdline += ["--build-dep-resolver", self.breq.cget("Build-Dep-Resolver"),
                         "--keyid", mini_buildd.get_daemon().model.mbd_gnupg().get_first_sec_key(),
                         "--nolog",
                         "--log-external-command-output",
                         "--log-external-command-error"]

        if self.breq.cget("Arch-All"):
            self.cmdline += ["--arch-all"]
        else:
            self.cmdline += ["--no-arch-all"]  # Be sure to set explicitly: sbuild >= 0.77 does not seem to use this as default any more?

        lintian_check = self.breq.check_mode("lintian")
        if lintian_check.mode.value > 0:
            self.cmdline += ["--run-lintian"]
            self.cmdline += ["--lintian-opts", lintian_check.lintian_options(self.breq.dist.codename)]
            self.cmdline += ["--lintian-opts", self.breq.check_extra_options("lintian")]
        else:
            self.cmdline += ["--no-run-lintian"]

        piuparts_check = self.breq.check_mode("piuparts")
        if piuparts_check.mode.value > 0:
            self.cmdline += ["--run-piuparts"]
            self.cmdline += ["--piuparts-opts", f"--schroot {self.breq.schroot_name()} --dpkg-force-confdef --warn-on-debsums-errors --warn-on-leftovers-after-purge --warn-on-others --keep-sources-list"]
            self.cmdline += ["--piuparts-opts", self.breq.check_extra_options("piuparts")]
        else:
            self.cmdline += ["--no-run-piuparts"]

        autopkgtest_check = self.breq.check_mode("autopkgtest")
        if autopkgtest_check.mode.value > 0:
            self.cmdline += ["--run-autopkgtest"]
            self.cmdline += ["--autopkgtest-opts", f"-- schroot {self.breq.schroot_name()}"]
            self.cmdline += ["--autopkgtest-root-args", ""]
        else:
            self.cmdline += ["--no-run-autopkgtest"]

        if "sbuild" in mini_buildd.config.DEBUG:
            self.cmdline += ["--debug"]

        self.cmdline += [self.breq.dfn.dsc()]

    BUILDLOG_STATUS_REGEX = re.compile("^(Status|Lintian|Piuparts|Autopkgtest|Build-Time|Package-Time|Space|Build-Space): [^ ]+.*$")

    def run(self, bres):
        """
        Run sbuild && update buildresult from build log.

        .. note:: This will iterate all lines of the build log,
                  and parse out the selection of sbuild's summary status
                  we need.  In case the build log above does write the
                  same output like 'Status: xyz', sbuild's correct status
                  at the bottom will override this later.  Best thing,
                  though, would be if sbuild would eventually provide a
                  better way to get these values.
        """
        # Run sbuild
        with mini_buildd.fopen(self.buildlog_file_path, "w+") as buildlog, subprocess.Popen(self.cmdline,
                                                                                            cwd=self.breq.builds_path.full,
                                                                                            env=mini_buildd.call.taint_env({"HOME": self.breq.builds_path.full,
                                                                                                                            "GNUPGHOME": mini_buildd.config.ROUTES["home"].path.join(".gnupg"),
                                                                                                                            "DEB_BUILD_OPTIONS": self.breq.cget("Deb-Build-Options", ""),
                                                                                                                            "DEB_BUILD_PROFILES": self.breq.cget("Deb-Build-Profiles", "")}),
                                                                                            stdout=buildlog,
                                                                                            stderr=subprocess.STDOUT) as self.call:
            retval = self.call.wait()

        # Update bres from buildlog
        bres.add_file(self.buildlog_file_path)
        with mini_buildd.fopen(self.buildlog_file_path, errors="replace") as f:
            for line in f:
                if self.BUILDLOG_STATUS_REGEX.match(line):
                    LOG.debug(f"Build log line detected as build status: {line.strip()}")
                    s = line.split(":")
                    bres.cset("Sbuild-" + s[0], s[1].strip())

        # Guarantee to set 'failed' sbuild status if retval != 0 (still deliver sbuild status from buildlog if there is one).
        if retval != 0:
            status = bres.cget("Sbuild-Status")
            bres.cset("Sbuild-Status", f"failed with retval={retval} ({status})")

    def cancel(self):
        if self.call is not None:
            LOG.info(f"Terminating sbuild process for {self.breq}...")
            self.call.terminate()
