#!/usr/bin/python3 -u
# -*- coding: utf-8 -*-
# WARNING: python -u means unbuffered I/O without it the messages are
# passed to the parent asynchronously which looks bad in clients.

import sys
import os
import errno
import getopt
from reportclient import log2, set_verbosity, error_msg_and_die, error_msg
import time
from reportclient.debuginfo import filter_installed_debuginfos, build_ids_to_paths, clean_up
import problem
import signal

# everything was ok
RETURN_OK = 0
# serious problem, should be logged somewhere
RETURN_FAILURE = 2

GETTEXT_PROGNAME = "abrt"
import locale
import gettext

class Config:
    verbosity = 0
    tmp_dir = None
    fbuild_ids = "build_ids"
    cachedirs = []
    size_mb = 4096
    keeprpms = False
    noninteractive = False
    exact_fls = False
    missing = None
    repo_pattern = "*debug*"
    pkgmgr = None
    releasever = None

_ = lambda x: gettext.gettext(x)

def init_gettext():
    try:
        locale.setlocale(locale.LC_ALL, "")
    except locale.Error:
        os.environ['LC_ALL'] = 'C'
        locale.setlocale(locale.LC_ALL, "")
    gettext.bindtextdomain(GETTEXT_PROGNAME, '/usr/share/locale')
    gettext.textdomain(GETTEXT_PROGNAME)

def measure_disposable_files(cache_dir, subdirectories, build_paths):
    """
    Compute the sum of the sizes of debuginfo files which are not needed anymore.
    """

    size_sum = 0

    for subdir in subdirectories:
        for dirpath, _, filenames in os.walk(os.path.join(cache_dir, subdir)):
            for name in filenames:
                if not name.endswith(".debug"):
                    continue
                full_path = os.path.join(dirpath, name)
                if os.path.isfile(full_path) and full_path not in build_paths:
                    size_sum += os.stat(full_path).st_size / (1024 * 1024)

    return size_sum

def trim_files(cache_dir, size, build_paths):
    """
    Call abrt-action-trim-files to clean up at least 'size' MiB worth of files
    inside 'cache_dir' leaving 'build_paths' intact.
    """

    try:
        pid = os.fork()
        if pid == 0:
            argv = ["abrt-action-trim-files", "-f", "%um:%s" % (size, cache_dir), "--"]
            argv.extend(build_paths)
            log2("abrt-action-trim-files %s", argv)
            os.execvp("abrt-action-trim-files", argv)
            error_msg_and_die("Can't execute '%s'", "abrt-action-trim-files")
        if pid > 0:
            os.waitpid(pid, 0)
    except Exception as e:
        error_msg("Can't execute abrt-action-trim-files: %s", e)

def run(config):
    missing = config.missing
    b_ids = []

    if missing == None:
        fin = sys.stdin
        if config.fbuild_ids != "-":
            try:
                fin = open(config.fbuild_ids, "r")
            except IOError as ex:
                error_msg_and_die(_("Can't open {0}: {1}").format(config.fbuild_ids, ex))
        for line in fin.readlines():
            b_ids.append(line.strip('\n'))

        if not b_ids:
            return RETURN_FAILURE

        # Delete oldest/biggest files from cachedir.
        # (Note that we need to do it before we check for missing debuginfos)
        #
        # We can do it as a separate step in report_event.conf, but this
        # would require setuid'ing abrt-action-trim-files to abrt:abrt.
        # Since we (via abrt-action-install-debuginfo-to-abrt-cache)
        # are already running setuid,
        # it makes sense to NOT setuid abrt-action-trim-files too,
        # but instead run it as our child:
        sys.stdout.flush()
        build_paths = build_ids_to_paths(config.cachedirs[0], b_ids)
        print("Cleaning cache...")
        try:
            pid = os.fork()
            if pid == 0:
                argv = ["abrt-action-trim-files", "-f", "%um:%s" % (config.size_mb, config.cachedirs[0]), "--"]
                argv.extend(build_paths)
                log2("abrt-action-trim-files %s", argv);
                os.execvp("abrt-action-trim-files", argv);
                error_msg_and_die("Can't execute '%s'", "abrt-action-trim-files");
            if pid > 0:
                os.waitpid(pid, 0);
                print("Cache cleaning has finished")
        except Exception as e:
            error_msg("Can't execute abrt-action-trim-files: %s", e);

        missing = filter_installed_debuginfos(b_ids, config.cachedirs)

    exact_file_missing = False
    result = RETURN_OK
    if missing:
        log2("%s", missing)
        if len(b_ids) > 0:
            print(_("Coredump references {0} debuginfo files").format(len(b_ids)))
        else:
            # Only --exact FILE[:FILE2]... was specified
            print(_("{0} of debuginfo files are not installed").format(len(missing)))

        download_class = None
        if config.pkgmgr == "dnf":
            from reportclient.dnfdebuginfo import DNFDebugInfoDownload
            download_class = DNFDebugInfoDownload
        elif config.pkgmgr == "yum":
            from reportclient.yumdebuginfo import YumDebugInfoDownload
            download_class = YumDebugInfoDownload
        else:
            sys.stderr.write(_("Invalid configuration of CCpp addon, unsupported Package manager: '%s'") % (config.pkgmgr))
            return RETURN_FAILURE

        # TODO: should we pass keep_rpms=keeprpms to DebugInfoDownload here??
        try:
            downloader = download_class(cache=config.cachedirs[0], tmp=config.tmp_dir,
                                    noninteractive=config.noninteractive,
                                    repo_pattern=config.repo_pattern,
                                    releasever=config.releasever)
            downloader.find_packages(missing)

            print(_("Going to install {0} debuginfo packages")
                  .format(downloader.get_package_count()))

            # Check how much space we need in cache
            # If we don't have enough space, find files we need to keep in cache
            # and start removing all others until we have enough space.
            # Before removing also check, if we are able to make enough space.
            # It does not make sense to delete everything just to find out we
            # still need more space
            res = os.statvfs(config.cachedirs[0])
            free_space = float(res.f_bsize * res.f_bavail) / (1024 * 1024)
            all_space = float(res.f_bsize * res.f_blocks) / (1024 * 1024)
            install_size = downloader.get_install_size() / (1024 * 1024)

            if install_size > free_space:
                debuginfo_dirs = ["usr/lib/debug/.build-id", "usr/lib/.build-id"]
                disposable_size = measure_disposable_files(config.cachedirs[0], debuginfo_dirs, build_paths)

                if disposable_size + free_space >= install_size:
                    config.size_mb = all_space - install_size
                    trim_files(config.cachedirs[0], config.size_mb, build_paths)

            result = downloader.download(missing, download_exact_files=config.exact_fls)

            # make sure that all downloaded directories are writeable by abrt group
            for root, dirs, files in os.walk(config.cachedirs[0]):
                for walked_dir in dirs:
                    os.chmod(os.path.join(root, walked_dir), 0o775)

        except OSError as ex:
            if ex.errno == errno.EPIPE:
                raise
            error_msg_and_die("Can't download debuginfos: %s", ex)

        if config.exact_fls:
            for bid in missing:
                if not os.path.isfile(bid):
                    print(_("Missing requested file: {0}").format(bid))
                    exact_file_missing = True

        missing = filter_installed_debuginfos(b_ids, config.cachedirs)
        for bid in missing:
            print(_("Missing debuginfo file: {0}").format(bid))

    if not missing and not exact_file_missing:
        print(_("All debuginfo files are available"))

    if exact_file_missing:
        result = RETURN_FAILURE

    return result

if __name__ == "__main__":
    config = Config()

    # localization
    init_gettext()

    help_text = _(
            "Usage: %s [-vy] [--ids=BUILD_IDS_FILE] [--pkgmgr=(yum|dnf)]\n"
            "       [--tmpdir=TMPDIR] [--cache=CACHEDIR[:DEBUGINFODIR1:DEBUGINFODIR2...]] [--size_mb=SIZE]\n"
            "       [-e, --exact=PATH[:PATH...]]\n"
            "       [--releasever=RELEASEVER] [--repo=PATTERN]\n"
            "\n"
            "Installs debuginfos for all build-ids listed in BUILD_IDS_FILE\n"
            "to CACHEDIR, using TMPDIR as temporary staging area.\n"
            "Old files in CACHEDIR are deleted until it is smaller than SIZE.\n"
            "\n"
            "Reads configuration from /etc/abrt/plugins/CCpp.conf\n"
            "\n"
            "    -v          Be verbose\n"
            "    -y          Noninteractive, assume 'Yes' to all questions\n"
            "    --ids       Default: build_ids\n"
            "    --tmpdir    Default: /var/tmp/abrt-tmp-debuginfo-RANDOM_SUFFIX\n"
            "    --cache     Colon separated list of directories. The first one is used for\n"
            "                saving installed debuginfos.\n"
            "                Default: /var/cache/abrt-di\n"
            "    --size_mb   Default: 4096\n"
            "    --pkgmgr    Default: PackageManager from CCpp.conf or 'dnf'\n"
            "    -e,--exact  Download only specified files\n"
            "    --repo      Pattern to use when searching for repos.\n"
            "                Default: *debug*\n"
            "    --releasever RELEASEVER\n"
            "                Pass this OS version to package managers.\n"
            # --keeprpms is not documented yet because it's a NOP so far
    ) % os.path.basename(sys.argv[0])

    try:
        opts, args = getopt.getopt(sys.argv[1:], "vyhe",
                ["help", "ids=", "cache=", "size_mb=", "tmpdir=", "keeprpms",
                 "exact=", "repo=", "pkgmgr=", "releasever="])
    except getopt.GetoptError as err:
        print(err) # prints something like "option -a not recognized"
        sys.exit(RETURN_FAILURE)

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            print(help_text)
            sys.exit(RETURN_OK)
        elif opt == "-v":
            config.verbosity += 1
        elif opt == "-y":
            config.noninteractive = True
        elif opt == "--ids":
            config.fbuild_ids = arg
        elif opt == "--cache":
            config.cachedirs = arg.split(':')
        elif opt == "--size_mb":
            try:
                config.size_mb = int(arg)
            except:
                pass
        elif opt == "--tmpdir":
            config.tmp_dir = arg
        elif opt == "--keeprpms":
            config.keeprpms = True
        # --exact takes precedence over --ids
        elif opt in ("-e", "--exact"):
            config.missing = arg.split(':')
            config.exact_fls = True
        elif opt == "--repo":
            config.repo_pattern = arg
        elif opt == "--pkgmgr":
            config.pkgmgr = arg
        elif opt == "--releasever":
            config.releasever = arg

    set_verbosity(config.verbosity)

    if not config.cachedirs:
        config.cachedirs = ["/var/cache/abrt-di"]
        try:
            conf = problem.load_plugin_conf_file("CCpp.conf")
        except OSError as ex:
            print(ex)
        else:
            config.cachedirs = conf.get("DebuginfoLocation", config.cachedirs[0]).split(":")

    if not config.tmp_dir:
        # security people prefer temp subdirs in app's private dir, like /var/run/abrt
        # and we switched to /tmp but Fedora feature tmp-on-tmpfs appeared, hence we must
        # not use /tmp for potential big data anymore
        config.tmp_dir = "/var/tmp/abrt-tmp-debuginfo-%s.%u" % (time.strftime("%Y-%m-%d-%H:%M:%S"), os.getpid())

    if not config.pkgmgr:
        try:
            conf = problem.load_plugin_conf_file("CCpp.conf")
        except OSError as ex:
            sys.stderr.write(str(ex))
            config.pkgmgr = "dnf"
        else:
            config.pkgmgr = conf.get("PackageManager", "dnf").lower()

    def sigterm_handler(signum, frame):
        clean_up(config.tmp_dir, silent=True)
        sys.exit(RETURN_OK)

    def sigint_handler(signum, frame):
        clean_up(config.tmp_dir)
        print("\n{0}".format(_("Exiting on user command")))
        sys.stdout.flush()
        sys.exit(RETURN_OK)

    # abrt-server can send SIGTERM to abort the download
    signal.signal(signal.SIGTERM, sigterm_handler)
    # ctrl-c
    signal.signal(signal.SIGINT, sigint_handler)

    try:
        sys.exit(run(config))
    # report-gtk likely closed the pipes on us (user canceled).
    # The reason we get SIGPIPEd is that this script runs under a different user,
    # so the most common case is lack of permissions to SIGTERM successfully.
    except BrokenPipeError:
        clean_up(config.tmp_dir, silent=True)
        sys.exit(RETURN_OK)
