import os
import re
import subprocess
import logging

import mini_buildd.call
from mini_buildd.files import Dir, PerlModule, ShScript, BashScript, AutoScript, SourcesList

LOG = logging.getLogger(__name__)


#: Build dir constants.
CONFIG_DIR = ".config"
CONFIG_APT_KEYS = "apt_keys"
CONFIG_APT_PREFERENCES = "apt_preferences"
CONFIG_APT_SOURCES_LIST = "apt_sources.list"
CONFIG_CHROOT_SETUP_SCRIPT = "chroot_setup_script"
CONFIG_SBUILDRC_SNIPPET = "sbuildrc_snippet"
CONFIG_SSL_CERT = "ssl_cert"

#: 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"
APT_TRUSTED_GPGD = "/etc/apt/trusted.gpg.d"
APT_CONFD = "/etc/apt/apt.conf.d"

#: Helper to compute CONFIG_DIR in bash code
BASH_CONFIG_DIR = f"${{0%/*/*}}/{CONFIG_DIR}"


class Blocks(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 line(self, extra_option, top=False):
        self.validate(extra_option)
        return f"{self.extra_option(top=top)}: {extra_option}"

    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), "")))


CONFIG_BLOCKS = ConfigBlocks()

CONFIG_BLOCKS.add(PerlModule(
    "set-default-path",
    description="auto: Always set $path, so config blocks may add to it (this is sbuild's default path)",
    snippet="""\
$path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games';
"""))

CONFIG_BLOCKS.add(PerlModule(
    "no-apt-update",
    description="auto: Disable sbuild's internal apt update (we do a custom update later via chroot-setup commands)",
    snippet="""\
$apt_update = 0;
"""))

CONFIG_BLOCKS.add(PerlModule(
    "apt-allow-unauthenticated",
    description="auto(if configured): Allow unauthenticated apt toggle (from buildrequest)",
    snippet="""\
$apt_allow_unauthenticated = 1;
"""))

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

SETUP_BLOCKS = SetupBlocks()

SETUP_BLOCKS.add(ShScript(
    "show-sbuildrc",
    description="auto-top: Print used '.sbuildrc' (FYI)",
    snippet=f"""\
cat "{BASH_CONFIG_DIR}/../.sbuildrc"
"""))

SETUP_BLOCKS.add(BashScript(
    "apt-keys",
    description=f"auto-top: Copy APT keys from buildrequest to ``{APT_TRUSTED_GPGD}``",
    snippet=f"""\
mkdir -p -v "{APT_TRUSTED_GPGD}"
cp -v "{BASH_CONFIG_DIR}/{CONFIG_APT_KEYS}" "{APT_TRUSTED_GPGD}/mini-buildd-buildrequest.asc"
"""))

SETUP_BLOCKS.add(ShScript(
    "apt-allow-unauthenticated",
    description="auto-top(if configured): Allow unauthenticated APT",
    snippet=f"""
printf 'APT::Get::AllowUnauthenticated \"true\";\\n' >'{APT_CONFD}/10mbd-allow-unauthenticated'
"""))

SETUP_BLOCKS.add(BashScript(
    "apt-setup-https",
    description="auto(if any https sources): Enable APT https support",
    snippet=f"""\
{APT_GET} install ca-certificates apt-transport-https
cp -v "{BASH_CONFIG_DIR}/{CONFIG_SSL_CERT}" /usr/local/share/ca-certificates/mini-buildd-repo.crt
/usr/sbin/update-ca-certificates
"""))

SETUP_BLOCKS.add(ShScript(
    "apt-setup",
    description="auto: Setup APT sources and preferences",
    snippet=f"""\
cp -v "{BASH_CONFIG_DIR}/{CONFIG_APT_SOURCES_LIST}" /etc/apt/sources.list
cat /etc/apt/sources.list

cp -v "{BASH_CONFIG_DIR}/{CONFIG_APT_PREFERENCES}" /etc/apt/preferences
cat /etc/apt/preferences
"""))

SETUP_BLOCKS.add(ShScript(
    "apt-update",
    description="auto: Update APT",
    snippet=f"""\
{APT_GET} update
apt-cache policy
"""))

# All below need explicit opt-in
SETUP_BLOCKS.add(ShScript(
    "ccache",
    description="Enables use of ccache (for builds on the same instance) -- also needs the 'ccache' config block",
    snippet=f"""\
{APT_GET} install ccache
"""))

SETUP_BLOCKS.add(ShScript(
    "eatmydata",
    description="Try to minimize to-disk-syncs -- may improve speed",
    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(BashScript(
    "apt-no-check-valid-until",
    description="compat(< stretch): Disable APT's 'Check-Valid-Until' globally (only needed where this option can't be given as apt line option)",
    snippet=f"""\
printf "Acquire::Check-Valid-Until \\"false\\";\\n" >"{APT_CONFD}/10mbd-no-check-valid-until"
"""))

SETUP_BLOCKS.add(BashScript(
    "apt-binary-keys",
    description="compat(<= jessie, <= yakketti): Convert APT keys to binary",
    snippet=f"""\
{APT_GET} update
{APT_GET} install gnupg
gpg --dearmor "{APT_TRUSTED_GPGD}/mini-buildd-buildrequest.asc"
"""))

SETUP_BLOCKS.add(BashScript(
    "apt-key-add",
    description="compat(<= squeeze): Add APT keys via ``apt-key add``",
    snippet=f"""\
apt-key add "{BASH_CONFIG_DIR}/{CONFIG_APT_KEYS}"
"""))

SETUP_BLOCKS.add(ShScript(
    "bash-devices",
    description="compat(bash < 3.0-17 (2005)): Fixup /dev/fd|stdin|stdout|stderr. Needed for *building* bash 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(BashScript(
    "schroot-shm",
    description="compat(schroot < 1.6.10-3): Fixup /dev/shm mount. 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(ShScript(
    "sun-java6-license",
    description="compat(< 2011): 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(ShScript(
    "apt-repair",
    description="compat,experimental: Try hard to make APT work again if stuck",
    snippet="""\
rm -v -r -f /var/lib/apt/lists/*
mkdir /var/lib/apt/lists/partial  # <= in lenny, apt would fail if missing
apt-get clean
"""))

SETUP_BLOCKS.add(ShScript(
    "apt-urold",
    description="compat,experimental(< wheezy): Try some hard configs for urold releases (squeeze or older) to make APT work",
    snippet=f"""\
printf "%s\\n" "Acquire::http::No-Cache true;" "Acquire::http::Pipeline-Depth 0;" "APT::Get::Force-Yes true;" >"{APT_CONFD}/00mbd-urold"
"""))


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_DIR])
        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.apt_allow_unauthenticated = breq.cget('Apt-Allow-Unauthenticated', "0") == "1"

        self.call = None

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

        # Top, auto
        sbuildrc.add(CONFIG_BLOCKS["set-default-path"].get(snippet=True))

        # Top, from distribution config
        CONFIG_BLOCKS.configure(sbuildrc, self.breq.cget, top=True)

        # auto
        sbuildrc.add(CONFIG_BLOCKS["no-apt-update"].get(snippet=True))
        if self.apt_allow_unauthenticated:
            sbuildrc.add(CONFIG_BLOCKS["apt-allow-unauthenticated"].get(snippet=True))

        # From distribution config
        CONFIG_BLOCKS.configure(sbuildrc, self.breq.cget, top=False)

        # Custom script from distribution config
        sbuildrc.add_file(self.config_path.join(CONFIG_SBUILDRC_SNIPPET), description="Custom sbuild config (from Distribution)")

        sbuildrc.save_as(self.sbuildrc_file_path)

        #
        # Generate setup.d/
        #
        setup_d = Dir(self.breq.builds_path.join(".setup.d"), enumerate_file_names=0)

        # Top, auto
        setup_d.add(SETUP_BLOCKS["show-sbuildrc"])
        setup_d.add(SETUP_BLOCKS["apt-keys"])
        if self.apt_allow_unauthenticated:
            setup_d.add(SETUP_BLOCKS["apt-allow-unauthenticated"])

        # Top, from distribution config
        SETUP_BLOCKS.configure(setup_d, self.breq.cget, top=True)

        # auto
        if SourcesList.has_https(self.config_path.join(CONFIG_APT_SOURCES_LIST)):
            setup_d.add(SETUP_BLOCKS["apt-setup-https"])
        setup_d.add(SETUP_BLOCKS["apt-setup"])
        setup_d.add(SETUP_BLOCKS["apt-update"])

        # From distribution config
        SETUP_BLOCKS.configure(setup_d, self.breq.cget, top=False)

        # Custom script from distribution config
        custom_buildrequest = self.config_path.join(CONFIG_CHROOT_SETUP_SCRIPT)
        if os.path.getsize(custom_buildrequest) > 0:
            setup_d.add(AutoScript(custom_buildrequest))

        setup_d.save()

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

        for name in setup_d.keys():
            script = os.path.join(setup_d.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.

        .. important:: 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 'error' sbuild status if retval != 0 and 'Sbuild-Status' could not be parsed from buildlog
        if retval != 0 and not bres.cget("Sbuild-Status"):
            bres.cset("Sbuild-Status", "error")

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