#!/usr/bin/python3 -u
# pylint: disable=missing-module-docstring,invalid-name
#
# Copyright (c) 2008--2017 Red Hat, Inc.
# Copyright (c) 2011 SUSE LLC
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#

import re

try:
    #  python2
    # pylint: disable-next=useless-import-alias
    import StringIO as StringIO
except ImportError:
    #  python3
    import io as StringIO
import json
# pylint: disable-next=unused-import
import shutil
import sys
import os
from optparse import OptionParser
import datetime

from spacewalk.satellite_tools.syncLib import initEMAIL_LOG

LOCK = None

log_path = "/var/log/rhn/reposync.log"


# pylint: disable-next=invalid-name
def systemExit(code, msg=None):
    "Exit with a code and optional message(s). Saved a few lines of code."
    sys.stderr.write(str(msg) + "\n")
    sys.exit(code)


try:
    from rhn import rhnLockfile
    from uyuni.common.context_managers import cfg_component
    # pylint: disable-next=ungrouped-imports
    from spacewalk.common import rhnLog
    from spacewalk.common.rhnConfig import CFG, initCFG
    from spacewalk.satellite_tools import reposync
    from spacewalk.satellite_tools.syncLib import log, log2disk
except KeyboardInterrupt:
    systemExit(0, "\nUser interrupted process.")
except ImportError:
    systemExit(1, "Unable to find code tree.\n" "Path not correct? " + str(sys.path))


# pylint: disable-next=invalid-name
def releaseLOCK():
    global LOCK
    if LOCK:
        LOCK.release()
        LOCK = None


def main():

    # quick check to see if you are a super-user.
    if os.getuid() != 0:
        systemExit(8, "ERROR: must be root to execute.")

    parser = OptionParser()
    parser.add_option(
        "-l",
        "--list",
        action="store_true",
        dest="list",
        help="List the custom channels with the associated repositories.",
    )
    parser.add_option(
        "-s",
        "--show-packages",
        action="store_true",
        dest="show_packages",
        help="List all packages in a specified channel.",
    )
    parser.add_option(
        "-u",
        "--url",
        action="append",
        dest="url",
        default=[],
        help="The url of the repository. Can be used multiple times.",
    )
    parser.add_option(
        "-c",
        "--channel",
        action="append",
        dest="channel_label",
        help="The label of the channel to sync packages to. Can be used multiple times.",
    )
    parser.add_option(
        "-p",
        "--parent-channel",
        action="append",
        dest="parent_label",
        default=[],
        help="Synchronize the parent channel and all its child channels.",
    )
    parser.add_option(
        "-d",
        "--dry-run",
        action="store_true",
        dest="dry_run",
        help="Test run. No sync takes place.",
    )
    parser.add_option(
        "--latest",
        action="store_true",
        dest="latest",
        help="Sync latest packages only. Use carefully - you might need to fix some dependencies on your own.",
    )
    parser.add_option(
        "-g", "--config", action="store", dest="config", help="Configuration file"
    )
    parser.add_option(
        "-t",
        "--type",
        action="store",
        dest="repo_type",
        help='Force type of repository ("yum", "uln" and "deb" are supported)',
    )
    parser.add_option(
        "-f",
        "--fail",
        action="store_true",
        dest="fail",
        default=False,
        help="If a package import fails, fail the entire operation",
    )
    parser.add_option(
        "-n",
        "--non-interactive",
        action="store_true",
        dest="noninteractive",
        default=False,
        help="Do not ask anything, use default answers",
    )
    parser.add_option(
        "-i",
        "--include",
        action="callback",
        callback=reposync.set_filter_opt,
        type="str",
        nargs=1,
        dest="filters",
        default=[],
        help="Comma or space separated list of included packages or package groups.",
    )
    parser.add_option(
        "-e",
        "--exclude",
        action="callback",
        callback=reposync.set_filter_opt,
        type="str",
        nargs=1,
        dest="filters",
        default=[],
        help="Comma or space separated list of excluded packages or package groups.",
    )
    parser.add_option(
        "",
        "--no-strict",
        action="store_true",
        help="do not unlink packages when deleted from repository",
        dest="no_strict",
    )
    parser.add_option(
        "",
        "--email",
        action="store_true",
        help="e-mail a report of what was synced/imported",
    )
    parser.add_option(
        "",
        "--traceback-mail",
        action="store",
        help="alternative email address(es) for sync output (--email option)",
    )
    parser.add_option(
        "",
        "--no-errata",
        action="store_true",
        dest="no_errata",
        default=False,
        help="Do not sync errata",
    )
    parser.add_option(
        "",
        "--no-packages",
        action="store_true",
        dest="no_packages",
        default=False,
        help="Do not sync packages",
    )
    parser.add_option(
        "",
        "--sync-kickstart",
        action="store_true",
        dest="sync_kickstart",
        default=False,
        help="Sync kickstartable tree",
    )
    parser.add_option(
        "",
        "--force-all-errata",
        action="store_true",
        dest="force_all_errata",
        default=False,
        help="Process metadata of all errata, not only missing.",
    )
    parser.add_option(
        "",
        "--batch-size",
        action="store",
        help="max. batch size for package import (debug only)",
    )
    parser.add_option(
        "-Y",
        "--deep-verify",
        action="store_true",
        dest="deep_verify",
        default=False,
        help="Do not use cached package checksums",
    )
    parser.add_option(
        "-v",
        "--verbose",
        action="count",
        help="Verbose output. Possible to accumulate: -vvv",
    )
    # pylint: disable-next=unused-variable
    (options, args) = parser.parse_args()

    global LOCK
    try:
        LOCK = rhnLockfile.Lockfile("/run/spacewalk-repo-sync.pid")
    except rhnLockfile.LockfileLockedException:
        systemExit(
            1,
            "ERROR: attempting to run more than one instance of "
            "spacewalk-repo-sync Exiting.",
        )

    log_level = options.verbose
    if log_level is None:
        # if no -v flag is passed, use "debug" setting from rhn.conf
        with cfg_component(None) as cfg:
            log_level = cfg.get("DEBUG", 0)

    # Apparently we need to call initCFG and have CFG available
    # to prevent some errors accessing CFG later on during package
    # import.
    initCFG("server.satellite")
    CFG.set("DEBUG", log_level)
    CFG.set("TRACEBACK_MAIL", options.traceback_mail or CFG.TRACEBACK_MAIL)

    if options.email:
        initEMAIL_LOG()
    rhnLog.initLOG(log_path, log_level)
    # pylint: disable-next=consider-using-f-string
    log2disk(0, "Command: %s" % str(sys.argv))

    l_params = ["no_errata", "sync_kickstart", "fail", "no-strict"]
    d_chan_repo = reposync.getChannelRepo()
    l_ch_custom = reposync.getCustomChannels()
    d_parent_child = reposync.getParentsChilds()
    d_ch_repo_sync = {}
    l_no_ch_repo_sync = []

    if options.list:
        log(0, "======================================")
        log(0, "|   Channel Label   |   Repository   |")
        log(0, "======================================")
        for ch in list(set(l_ch_custom) & set(d_chan_repo)):
            for repo in d_chan_repo[ch]:
                # pylint: disable-next=consider-using-f-string
                log(0, "%s | %s" % (ch, repo))
        for ch in list(set(l_ch_custom) - set(d_chan_repo)):
            # pylint: disable-next=consider-using-f-string
            log(0, "%s | No repository set" % ch)
        return 0

    if not options.channel_label and not options.parent_label and not options.config:
        systemExit(1, "--channel, --parent-channel or --config must be specifed.")

    if options.config:
        try:
            # pylint: disable-next=unspecified-encoding
            config_file = open(options.config).read()
            # strip  all whitespace
            config_file = re.sub(r"\s", "", config_file)
            config = json.load(StringIO.StringIO(config_file))

        # pylint: disable-next=broad-exception-caught
        except Exception as e:
            systemExit(
                # pylint: disable-next=consider-using-f-string
                1, "Configuration file is invalid, please check syntax. Error [%s]" % e
            )

        for key in l_params:
            if key in config and not getattr(options, key):
                setattr(options, key, config[key])

        # Channels
        if "channel" in config:
            for ch, repo in config["channel"].items():
                if not isinstance(repo, list):
                    systemExit(
                        1,
                        # pylint: disable-next=consider-using-f-string
                        "Configuration file is invalid, "
                        "{0}'s value needs to be a list.".format(ch),
                    )
                d_ch_repo_sync[ch] = repo

        if "parent_channel" in config:
            options.parent_label += config["parent_channel"]

    if options.channel_label and len(options.channel_label) > 0:
        for channel in options.channel_label:
            d_ch_repo_sync[channel] = options.url

    if options.parent_label:
        for pch in options.parent_label:
            if pch in d_parent_child:
                for ch in [pch] + d_parent_child[pch]:
                    if ch in l_ch_custom and ch not in d_ch_repo_sync:
                        d_ch_repo_sync[ch] = []
            else:
                # pylint: disable-next=consider-using-f-string
                systemExit(1, "Channel %s is not custom base channel." % pch)

    # pylint: disable-next=consider-using-dict-items
    for ch in d_ch_repo_sync:
        if ch not in l_ch_custom:
            # pylint: disable-next=consider-using-f-string
            systemExit(1, "Channel %s is not custom or does not exist." % ch)
        if not d_ch_repo_sync[ch] and not ch in d_chan_repo:
            # pylint: disable-next=consider-using-f-string
            log(0, "Channel %s Channel has no URL associated, skipping sync" % ch)
            l_no_ch_repo_sync.append(ch)

    for ch in l_no_ch_repo_sync:
        del d_ch_repo_sync[ch]

    if options.dry_run:
        log(0, "======================================")
        log(0, "|   Channel Label   |   Repository   |")
        log(0, "======================================")

        for ch, repo in list(d_ch_repo_sync.items()):
            if repo:
                # pylint: disable-next=consider-using-f-string
                log(0, " %s : %s" % (ch, ", ".join(repo)))
            else:
                # pylint: disable-next=consider-using-f-string
                log(0, " %s : %s" % (ch, ", ".join(d_chan_repo[ch])))

        log(0, "======================================")
        log(0, "|             Parameters             |")
        log(0, "======================================")
        for key in l_params:
            # pylint: disable-next=consider-using-f-string
            log(0, " %s: %s" % (key, str(getattr(options, key))))
        return 0

    if options.batch_size:
        try:
            batch_size = int(options.batch_size)
            if batch_size <= 0:
                raise ValueError()
        except ValueError:
            # pylint: disable-next=consider-using-f-string
            systemExit(1, "Invalid batch size: %s" % options.batch_size)

    reposync.clear_ssl_cache()

    total_time = datetime.timedelta()
    ret_code = 0
    for ch, repo in list(d_ch_repo_sync.items()):

        log(0, "======================================")
        # pylint: disable-next=consider-using-f-string
        log(0, "| Channel: %s" % ch)
        log(0, "======================================")
        log(0, "Sync of channel started.")
        log2disk(
            0,
            # pylint: disable-next=consider-using-f-string
            "Please check 'reposync/%s.log' for sync log of this channel." % ch,
            notimeYN=True,
        )
        sync = reposync.RepoSync(
            channel_label=ch,
            repo_type=options.repo_type,
            url=repo,
            fail=options.fail,
            strict=not options.no_strict,
            noninteractive=options.noninteractive,
            filters=options.filters,
            deep_verify=options.deep_verify,
            no_errata=options.no_errata,
            no_packages=options.no_packages,
            sync_kickstart=options.sync_kickstart,
            latest=options.latest,
            log_level=log_level,
            force_all_errata=options.force_all_errata,
            show_packages_only=options.show_packages,
        )
        if options.batch_size:
            sync.set_import_batch_size(options.batch_size)
        elapsed_time, channel_ret_code = sync.sync()
        if channel_ret_code != 0 and ret_code == 0:
            ret_code = channel_ret_code
        total_time += elapsed_time
        # Switch back to common log
        rhnLog.initLOG(log_path, log_level)
        log2disk(0, "Sync of channel completed.")

    # pylint: disable-next=consider-using-f-string,use-maxsplit-arg
    log(0, "Total time: %s" % str(total_time).split(".")[0])
    if options.email:
        reposync.send_mail()
    releaseLOCK()
    return ret_code


if __name__ == "__main__":
    try:
        sys.exit(abs(main() or 0))
    except KeyboardInterrupt:
        systemExit(1, "\nProcess has been interrupted.")
    except SystemExit as e:
        releaseLOCK()
        sys.exit(e.code)
    except Exception as e:
        releaseLOCK()
        raise
