#!/usr/bin/python3
# pylint: disable=missing-module-docstring,invalid-name
#
# Copyright (c) 2010--2015 Red Hat, Inc.
#
# 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 sys
import fnmatch
from optparse import OptionParser, Option
import re

from spacewalk.common.rhnConfig import cfg_component
from uyuni.common.cli import getUsernamePassword, xmlrpc_login, xmlrpc_logout

try:
    import xmlrpclib
except ImportError:
    import xmlrpc.client as xmlrpclib  # pylint: disable=F0401

try:
    import ConfigParser
except ImportError:
    import configparser as ConfigParser

DEFAULT_SERVER = "localhost"

DEFAULT_CONFIG = "/etc/rhn/spacewalk-common-channels.ini"

DEFAULT_REPO_TYPE = "yum"

CHANNEL_ARCH = {
    "aarch64": "channel-aarch64",
    "i386": "channel-ia32",
    "ia64": "channel-ia64",
    "sparc": "channel-sparc",
    "sparc64": "channel-sparc64",
    "alpha": "channel-alpha",
    "s390": "channel-s390",
    "s390x": "channel-s390x",
    "iSeries": "channel-iSeries",
    "pSeries": "channel-pSeries",
    "x86_64": "channel-x86_64",
    "ppc": "channel-ppc",
    "ppc64": "channel-ppc64",
    "ppc64le": "channel-ppc64le",
    "amd64-deb": "channel-amd64-deb",
    "arm64-deb": "channel-arm64-deb",
    "armhf-deb": "channel-armhf-deb",
    "ia32-deb": "channel-ia32-deb",
    "ia64-deb": "channel-ia64-deb",
    "sparc-deb": "channel-sparc-deb",
    "alpha-deb": "channel-alpha-deb",
    "s390-deb": "channel-s390-deb",
    "powerpc-deb": "channel-powerpc-deb",
    "arm-deb": "channel-arm-deb",
    "mips-deb": "channel-mips-deb",
}
CHANNEL_NAME_TO_ARCH = {
    "AArch64": "aarch64",
    "Alpha": "alpha",
    "Alpha Debian": "alpha-deb",
    "AMD64 Debian": "amd64-deb",
    "ARM64 Debian": "arm64-deb",
    "ARM HF Debian": "armhf",
    "arm Debian": "arm-deb",
    "ARM hard. FP": "armhfp",
    "ARM soft. FP": "arm",
    "IA-32": "i386",
    "IA-32 Debian": "ia32-deb",
    "IA-64": "ia64",
    "IA-64 Debian": "ia64-deb",
    "iSeries": "iSeries",
    "mips Debian": "mips-deb",
    "PowerPC Debian": "powerpc-deb",
    "PPC": "ppc",
    "PPC64LE": "ppc64le",
    "pSeries": "pSeries",
    "s390": "s390",
    "s390 Debian": "s390-deb",
    "s390x": "s390x",
    "Sparc": "sparc",
    "Sparc Debian": "sparc-deb",
    "x86_64": "x86_64",
}

SPLIT_PATTERN = "[ ,]+"


class ExtOptionParser(OptionParser):
    """extend OptionParser to print examples"""

    # pylint: disable-next=redefined-outer-name
    def __init__(self, examples=None, **kwargs):
        self.examples = examples
        OptionParser.__init__(self, **kwargs)

    def print_help(self):
        OptionParser.print_help(self)
        print("\n\n" + self.examples)


def connect(user, password, server):
    # pylint: disable-next=consider-using-f-string
    server_url = "https://%s/rpc/api" % server
    if server in ["localhost", "127.0.0.1"]:
        # pylint: disable-next=consider-using-f-string
        server_url = "http://%s/rpc/api" % server

    # pylint: disable-next=possibly-used-before-assignment
    if options.verbose and options.verbose > 2:
        client_verbose = options.verbose - 2
    else:
        client_verbose = 0
    if options.verbose:
        # pylint: disable-next=consider-using-f-string
        sys.stdout.write("Connecting to %s\n" % server_url)
    # pylint: disable-next=redefined-outer-name
    client = xmlrpclib.Server(server_url, verbose=client_verbose)
    options.user, password = getUsernamePassword(user, password)
    # pylint: disable-next=redefined-outer-name
    key = xmlrpc_login(client, options.user, password)
    return client, key


# pylint: disable-next=redefined-outer-name,redefined-outer-name,redefined-outer-name,redefined-outer-name
def add_channels(channels, section, arch, client):
    # pylint: disable-next=redefined-outer-name
    base_channels = [""]
    optional = [
        "activationkey",
        "base_channel_activationkey",
        "gpgkey_url",
        "gpgkey_id",
        "gpgkey_fingerprint",
        "repo_url",
        "yum_repo_label",
        "dist_map_release",
        "repo_type",
    ]
    mandatory = ["label", "name", "summary", "checksum", "arch", "section"]

    # pylint: disable-next=possibly-used-before-assignment
    config.set(section, "arch", arch)
    config.set(section, "section", section)
    if config.has_option(section, "base_channels"):
        base_channels = re.split(SPLIT_PATTERN, config.get(section, "base_channels"), 1)

    for base_channel in base_channels:
        config.set(section, "base_channel", base_channel)
        # pylint: disable-next=redefined-outer-name
        channel = {"base_channel": config.get(section, "base_channel")}

        if base_channel:
            if channel["base_channel"] in channels:
                # If channel exists at the INI file, use it.
                pass
            elif channel_exists(client, channel["base_channel"]):
                # If not, if cannel exists on the server, convert it
                channel_base_server = channel_get_details(
                    client, channel["base_channel"]
                )
                channels[channel["base_channel"]] = {
                    "label": channel_base_server["label"],
                    "name": channel_base_server["name"],
                    "summary": channel_base_server["summary"],
                    "arch": CHANNEL_NAME_TO_ARCH[channel_base_server["arch_name"]],
                    "checksum": channel_base_server["checksum_label"],
                    "gpgkey_url": channel_base_server["gpg_key_url"],
                    "gpgkey_id": channel_base_server["gpg_key_id"],
                    "gpgkey_fingerprint": channel_base_server["gpg_key_fp"],
                    "section": section,
                }
            else:
                # Otheriwse there isn't such base channel so skip also child
                continue
            # set base channel values so they can be used as macros
            for k, v in list(channels[channel["base_channel"]].items()):
                config.set(section, "base_channel_" + k, v)

        for k in optional:
            if config.has_option(section, k):
                channel[k] = config.get(section, k)
            else:
                channel[k] = ""
        for k in mandatory:
            channel[k] = config.get(section, k)
        channels[channel["label"]] = channel


# pylint: disable-next=redefined-outer-name
def channel_exists(client, channel_label):
    # check whether channel exists
    try:
        # pylint: disable-next=redefined-outer-name,possibly-used-before-assignment
        base_info = client.channel.software.isExisting(key, channel_label)
        return base_info
    # We should improve this as a connection failure would be the same
    # as not finding the channel
    # pylint: disable-next=unused-variable
    except xmlrpclib.Fault as e:
        return None


# pylint: disable-next=redefined-outer-name
def channel_get_details(client, channel_label):
    # get details for a channel
    try:
        # pylint: disable-next=redefined-outer-name
        base_info = client.channel.software.getDetails(key, channel_label)
        return base_info
    # We should improve this as a connection failure would be the same
    # as not finding the channel
    # pylint: disable-next=unused-variable
    except xmlrpclib.Fault as e:
        return None


# pylint: disable-next=redefined-outer-name
def get_existing_repos(client):
    result = {}
    try:
        user_repos = client.channel.software.listUserRepos(key)
        for repo in user_repos:
            result[repo["sourceUrl"]] = repo["label"]
    # pylint: disable-next=unused-variable
    except xmlrpclib.Fault as e:
        return None
    return result


if __name__ == "__main__":
    # options parsing
    usage = "usage: %prog [options] <channel1 glob> [<channel2 glob> ... ]"
    # pylint: disable-next=consider-using-f-string
    examples = """Examples:

Create Fedora 12 channel, its child channels and activation key limited to 10 servers:
    %(prog)s -u admin -p pass -k 10 'fedora12*'

Create Centos 5 with child channels only on x86_64:
    %(prog)s -u admin -p pass -a x86_64 'centos5*'

Create only Centos 4 base channels for intel archs:
    %(prog)s -u admin -p pass -a i386,x86_64 'centos4'

Create Spacewalk client child channel for every (suitable) defined base channel:
    %(prog)s -u admin -p pass 'spacewalk-client*'

Create everything as well as unlimited activation key for every channel:
    %(prog)s -u admin -p pass -k unlimited '*'
\n""" % {
        "prog": sys.argv[0]
    }

    option_list = [
        Option("-c", "--config", help="configuration file", default=DEFAULT_CONFIG),
        Option("-u", "--user", help="username"),
        Option("-p", "--password", help="password"),
        Option("-s", "--server", help="your spacewalk server", default=DEFAULT_SERVER),
        Option(
            "-k",
            "--keys",
            help="activation key usage limit -"
            + " 'unlimited' or number\n"
            + "(default: options is not set and activation keys"
            + " are not created at all)",
            dest="key_limit",
        ),
        Option(
            "-n",
            "--dry-run",
            help="perform a trial run with no changes made",
            action="store_true",
        ),
        Option("-a", "--archs", help="list of architectures"),
        Option("-v", "--verbose", help="verbose", action="count"),
        Option(
            "-l", "--list", help="print list of available channels", action="store_true"
        ),
        Option(
            "-d",
            "--default-channels",
            help="make base channels default channels for given OS version",
            action="store_true",
        ),
    ]

    # Read the configuration to figure out if channel should be synced automatically on creation
    sync_channels = True
    with cfg_component("java") as CFG:
        try:
            sync_channels = CFG.unify_custom_channel_management.lower() in [
                "1",
                "y",
                "true",
                "yes",
                "on",
            ]
        except (AttributeError, ValueError):
            pass

    parser = ExtOptionParser(usage=usage, option_list=option_list, examples=examples)
    (options, args) = parser.parse_args()
    config = ConfigParser.ConfigParser()
    config.read(options.config)

    if options.list:
        print("Available channels:")
        channel_list = config.sections()
        if channel_list:
            for channel in sorted(channel_list):
                channel_archs = config.get(channel, "archs")
                # pylint: disable-next=consider-using-f-string
                print(" %-20s %s" % (channel + ":", channel_archs))
        else:
            print(" [no channel available]")
        sys.exit(0)

    if not args:
        print(parser.print_help())
        parser.exit()

    try:
        client, key = connect(options.user, options.password, options.server)
        user_info = client.user.getDetails(key, options.user)
        org_id = user_info["org_id"]
    except xmlrpclib.Fault as e:
        if e.faultCode == 2950:
            sys.stderr.write("Either the password or username is incorrect.\n")
            sys.exit(2)
        else:
            raise

    channels = {}

    sections = []
    # sort base channels first and child last
    for section in config.sections():
        if config.has_option(section, "base_channels"):  # child
            sections.append(section)
        else:  # base
            sections.insert(0, section)
    for section in sections:
        archs = re.split(SPLIT_PATTERN, config.get(section, "archs"))
        if options.archs:
            # filter out archs not set on commandline
            archs = [a for a in archs if a in options.archs]
        for arch in archs:
            add_channels(channels, section, arch, client)

    # list of base_channels to deal with
    base_channels = {}
    # list of child_channels for given base_channel
    child_channels = {}
    # filter out non-matching channels
    for pattern in args:
        matching_channels = [
            n
            for n in list(channels.keys())
            if fnmatch.fnmatch(channels[n]["section"], pattern)
        ]
        for name in matching_channels:
            attr = channels[name]
            if "base_channel" in attr and attr["base_channel"]:
                if attr["base_channel"] not in base_channels:
                    base_channels[attr["base_channel"]] = False
                if attr["base_channel"] in child_channels:
                    child_channels[attr["base_channel"]].append(name)
                else:
                    child_channels[attr["base_channel"]] = [name]
            else:
                # this channel is base channel
                base_channels[name] = True
                if name not in child_channels:
                    child_channels[name] = []

    if not matching_channels:
        sys.stderr.write("No channels matching your selection.\n")
        sys.exit(2)

    existing_repo_urls = get_existing_repos(client)
    if existing_repo_urls is None:
        sys.stderr.write("Unable to get existing repositories from server.\n")
        sys.exit(2)

    for base_channel_label, create_channel in sorted(base_channels.items()):

        if create_channel:
            base_info = channels[base_channel_label]
            repo_type = (
                base_info.get("repo_type", DEFAULT_REPO_TYPE) or DEFAULT_REPO_TYPE
            )
            if options.verbose:
                sys.stdout.write(
                    # pylint: disable-next=consider-using-f-string
                    "Base channel '%s' - creating...\n" % base_info["name"]
                )
            if options.verbose and options.verbose > 1:
                sys.stdout.write(
                    # pylint: disable-next=consider-using-f-string
                    "* label=%s, summary=%s, arch=%s, repo_type=%s, checksum=%s\n"
                    % (
                        base_info["label"],
                        base_info["summary"],
                        base_info["arch"],
                        repo_type,
                        base_info["checksum"],
                    )
                )

            if not options.dry_run:
                try:
                    # create base channel
                    client.channel.software.create(
                        key,
                        base_info["label"],
                        base_info["name"],
                        base_info["summary"],
                        CHANNEL_ARCH[base_info["arch"]],
                        "",
                        base_info["checksum"],
                        {
                            "url": base_info["gpgkey_url"],
                            "id": base_info["gpgkey_id"],
                            "fingerprint": base_info["gpgkey_fingerprint"],
                        },
                    )
                    if base_info["repo_url"] in existing_repo_urls:
                        # use existing repo
                        client.channel.software.associateRepo(
                            key,
                            base_info["label"],
                            existing_repo_urls[base_info["repo_url"]],
                        )
                    else:
                        client.channel.software.createRepo(
                            key,
                            base_info["yum_repo_label"],
                            repo_type,
                            base_info["repo_url"],
                        )
                        client.channel.software.associateRepo(
                            key, base_info["label"], base_info["yum_repo_label"]
                        )

                    if sync_channels:
                        client.channel.software.syncRepo(key, base_info["label"])
                except xmlrpclib.Fault as e:
                    if e.faultCode == 1200:  # ignore if channel exists
                        # pylint: disable-next=consider-using-f-string
                        sys.stdout.write("INFO: %s exists\n" % base_info["label"])
                    else:
                        sys.stderr.write(
                            # pylint: disable-next=consider-using-f-string
                            "ERROR: %s: %s\n" % (base_info["label"], e.faultString)
                        )
                        sys.exit(2)

            if options.key_limit is not None:
                if options.verbose:
                    sys.stdout.write(
                        # pylint: disable-next=consider-using-f-string
                        "* Activation key '%s' - creating...\n" % (base_info["label"])
                    )
                if not options.dry_run:
                    # create activation key
                    if options.key_limit == "unlimited":
                        ak_args = (
                            key,
                            base_info["activationkey"],
                            base_info["name"],
                            base_info["label"],
                            [],
                            False,
                        )
                    else:
                        ak_args = (
                            key,
                            base_info["activationkey"],
                            base_info["name"],
                            base_info["label"],
                            int(options.key_limit),
                            [],
                            False,
                        )
                    try:
                        client.activationkey.create(*ak_args)
                    except xmlrpclib.Fault as e:
                        if e.faultCode != 1091:  # ignore if ak exists
                            sys.stderr.write(
                                # pylint: disable-next=consider-using-f-string
                                "ERROR: %s: %s\n" % (base_info["label"], e.faultString)
                            )
        else:
            # check whether channel exists
            if channel_exists(client, base_channel_label):
                base_info = channel_get_details(client, base_channel_label)
                # pylint: disable-next=consider-using-f-string
                sys.stdout.write("Base channel '%s' - exists\n" % base_info["name"])
            else:
                sys.stderr.write(
                    # pylint: disable-next=consider-using-f-string
                    "ERROR: %s could not be found at the server\n" % base_channel_label
                )

        if options.default_channels:
            try:
                client.distchannel.setMapForOrg(
                    key,
                    base_info["name"],
                    base_info["dist_map_release"],
                    base_info["arch"],
                    base_info["label"],
                )

            except xmlrpclib.Fault as e:
                sys.stderr.write(
                    # pylint: disable-next=consider-using-f-string
                    "ERROR: %s: %s\n" % (base_info["label"], e.faultString)
                )

        for child_channel_label in sorted(child_channels[base_channel_label]):
            child_info = channels[child_channel_label]
            repo_type = (
                child_info.get("repo_type", DEFAULT_REPO_TYPE) or DEFAULT_REPO_TYPE
            )
            if options.verbose:
                sys.stdout.write(
                    # pylint: disable-next=consider-using-f-string
                    "* Child channel '%s' - creating...\n" % child_info["name"]
                )
            if options.verbose and options.verbose > 1:
                sys.stdout.write(
                    # pylint: disable-next=consider-using-f-string
                    "** label=%s, summary=%s, arch=%s, parent=%s, repo_type=%s, checksum=%s\n"
                    % (
                        child_info["label"],
                        child_info["summary"],
                        child_info["arch"],
                        base_channel_label,
                        repo_type,
                        child_info["checksum"],
                    )
                )

            if not options.dry_run:
                try:
                    # create child channels
                    client.channel.software.create(
                        key,
                        child_info["label"],
                        child_info["name"],
                        child_info["summary"],
                        CHANNEL_ARCH[child_info["arch"]],
                        base_channel_label,
                        child_info["checksum"],
                        {
                            "url": child_info["gpgkey_url"],
                            "id": child_info["gpgkey_id"],
                            "fingerprint": child_info["gpgkey_fingerprint"],
                        },
                    )
                    if child_info["repo_url"] in existing_repo_urls:
                        # use existing repo
                        client.channel.software.associateRepo(
                            key,
                            child_info["label"],
                            existing_repo_urls[child_info["repo_url"]],
                        )
                    else:
                        client.channel.software.createRepo(
                            key,
                            child_info["yum_repo_label"],
                            repo_type,
                            child_info["repo_url"],
                        )
                        client.channel.software.associateRepo(
                            key, child_info["label"], child_info["yum_repo_label"]
                        )
                    if sync_channels:
                        client.channel.software.syncRepo(key, child_info["label"])
                except xmlrpclib.Fault as e:
                    if e.faultCode == 1200:  # ignore if channel exists
                        sys.stderr.write(
                            # pylint: disable-next=consider-using-f-string
                            "WARNING: %s: %s\n" % (child_info["label"], e.faultString)
                        )
                    else:
                        sys.stderr.write(
                            # pylint: disable-next=consider-using-f-string
                            "ERROR: %s: %s\n" % (child_info["label"], e.faultString)
                        )
                        sys.exit(2)

            if options.key_limit is not None:
                if (
                    "base_channel_activationkey" in child_info
                    and child_info["base_channel_activationkey"]
                ):
                    # pylint: disable-next=consider-using-f-string
                    activationkey = "%s-%s" % (
                        org_id,
                        child_info["base_channel_activationkey"],
                    )
                    if options.verbose:
                        sys.stdout.write(
                            # pylint: disable-next=consider-using-f-string
                            "** Activation key '%s' - adding child channel...\n"
                            % (activationkey)
                        )
                    if not options.dry_run:
                        try:
                            client.activationkey.addChildChannels(
                                key, activationkey, [child_info["label"]]
                            )
                        except xmlrpclib.Fault as e:
                            sys.stderr.write(
                                # pylint: disable-next=consider-using-f-string
                                "ERROR: %s: %s\n" % (child_info["label"], e.faultString)
                            )

        if options.verbose:
            # an empty line after channel group
            sys.stdout.write("\n")
        existing_repo_urls = get_existing_repos(client)
    if client is not None:
        # logout
        xmlrpc_logout(client, key)
