#!/usr/bin/python3
# pylint: disable=missing-module-docstring,invalid-name
#
# Module that removes channels from an installed satellite
#
#
# Copyright (c) 2008--2017 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 os
import fnmatch
import getpass
from optparse import Option, OptionParser

# quick check to see if you are a super-user.
if os.getuid() != 0:
    sys.stderr.write("ERROR: must be root to execute\n")
    sys.exit(8)

try:
    from rhn import rhnLockfile  # new place for rhnLockfile
# pylint: disable-next=bare-except
except:
    from spacewalk.common import rhnLockfile  # old place for rhnLockfile

# pylint: disable-next=wrong-import-position
from spacewalk.common.rhnLog import initLOG
# pylint: disable-next=wrong-import-position
from spacewalk.common.rhnConfig import initCFG
# pylint: disable-next=wrong-import-position
from spacewalk.server import rhnSQL

# pylint: disable-next=wrong-import-position
from spacewalk.satellite_tools.contentRemove import (
    __listChannels,
    __serverCheck,
    __kickstartCheck,
    delete_channels,
    UserError,
    __getMinionsByChannel,
    __applyChannelState,
    __clonnedChannels,
)

options_table = [
    Option("-v", "--verbose", action="count", help="Increase verbosity"),
    Option("-l", "--list", action="store_true", help="List defined channels and exit"),
    Option(
        "-c",
        "--channel",
        action="append",
        default=[],
        help="Delete this channel (can be present multiple times)",
    ),
    Option(
        "-a",
        "--channel-with-children",
        action="append",
        default=[],
        help="Delete this channel and its child channels (can be present multiple times)",
    ),
    Option(
        "-u",
        "--unsubscribe",
        action="store_true",
        help="Unsubscribe systems registered to the specified channels. Note: Credentials are needed in case of minions",
    ),
    Option(
        "--justdb",
        action="store_true",
        help="Delete only from the database, do not remove files from disk",
    ),
    Option(
        "--force",
        action="store_true",
        help="Remove the channel packages from any other channels too (Not Recommended)",
    ),
    Option(
        "-p",
        "--skip-packages",
        action="store_true",
        help="Do not remove package metadata or packages from the filesystem (Not Recommended)",
    ),
    Option(
        "--skip-kickstart-trees",
        action="store_true",
        help="Do not remove kickstart trees from the filesystem (Not Recommended).",
    ),
    Option(
        "--just-kickstart-trees",
        action="store_true",
        help="Remove only the kickstart trees for the channels specified.",
    ),
    Option(
        "--skip-channels",
        action="store_true",
        help="Remove only packages from channel not the channel itself.",
    ),
    Option("--username", help="Username"),
    Option("--password", help="Password"),
]

LOCK = []
LOCK_DIR = "/run"


def main():

    # pylint: disable-next=global-variable-not-assigned
    global LOCK
    # pylint: disable-next=global-variable-not-assigned
    global options_table
    parser = OptionParser(option_list=options_table)

    (options, args) = parser.parse_args()

    if args:
        for arg in args:
            # pylint: disable-next=consider-using-f-string
            sys.stderr.write("Not a valid option ('%s'), try --help\n" % arg)
        sys.exit(-1)

    if not (options.channel or options.list or options.channel_with_children):
        sys.stderr.write("Nothing to do\n")
        sys.exit(0)

    if not options.list:
        for command in ["spacewalk-remove-channel", "spacewalk-repo-sync"]:
            try:
                LOCK.append(
                    # pylint: disable-next=consider-using-f-string
                    rhnLockfile.Lockfile(os.path.join(LOCK_DIR, "%s.pid" % command))
                )
            except rhnLockfile.LockfileLockedException:
                # pylint: disable-next=consider-using-f-string
                print("ERROR: An instance of %s is running, exiting." % command)
                sys.exit(-1)

    initCFG("server")
    initLOG("stdout", options.verbose or 0)

    rhnSQL.initDB()

    dict_label, dict_parents = __listChannels()
    if options.list:
        keys = list(dict_parents.keys())
        keys.sort()
        for c in keys:
            print(c)
            for sub in dict_parents[c]:
                print("\t" + sub)
        sys.exit(0)

    # Verify if the channel is valid
    # pylint: disable-next=unused-variable
    base_channel = ""
    channels = {}
    for channel in options.channel:
        channels[channel] = None
        if channel in dict_parents:
            base_channel = channel

    for parent in options.channel_with_children:
        matchs = fnmatch.filter(dict_parents, parent)
        for parent in matchs:
            if parent in dict_parents:
                channels[parent] = None
                base_channel = parent
                for ch in dict_parents[parent]:
                    channels[ch] = None
        if not matchs:
            # pylint: disable-next=consider-using-f-string
            print("Unknown parent channel %s" % parent)
            sys.exit(-1)

    child_test_fail = False
    for channel in list(channels.keys()):
        if channel not in dict_label:
            # pylint: disable-next=consider-using-f-string
            print("Unknown channel %s" % channel)
            sys.exit(-1)
        if not options.skip_channels and not options.just_kickstart_trees:
            # Sanity check: verify subchannels are deleted as well if base
            # channels are selected
            if channel not in dict_parents:
                continue
            # this channel is a parent channel?
            children = []
            for subch in dict_parents[channel]:
                if subch not in channels:
                    child_test_fail = True
                    children.append(subch)
            if children:
                print(
                    # pylint: disable-next=consider-using-f-string
                    "Error: cannot remove channel %s: subchannel(s) exist: " % (channel)
                )
                for child in children:
                    print("\t\t\t" + child)

    clone_test_fail = False
    for channel in list(channels.keys()):
        clones_not_deleted = []
        for clone in __clonnedChannels(channel):
            # the clones channels will also be deleted?
            if clone not in channels:
                clone_test_fail = True
                clones_not_deleted.append(clone)
        if clones_not_deleted:
            print(
                # pylint: disable-next=consider-using-f-string
                "Error: cannot remove channel %s: clone channel(s) exist: " % (channel)
            )
            for clone_channel_error in clones_not_deleted:
                print("\t\t\t" + clone_channel_error)

    if child_test_fail or clone_test_fail:
        sys.exit(-1)
    if options.unsubscribe:
        if not options.username:
            raise UserError("Username not specified")
        if not options.password:
            options.password = getpass.getpass()
        affected_minions = __getMinionsByChannel(list(channels.keys()))

    if not options.skip_channels and not options.just_kickstart_trees:
        if __serverCheck(list(channels.keys()), options.unsubscribe):
            sys.exit(-1)

        if __kickstartCheck(list(channels.keys())):
            sys.exit(-1)

    try:
        delete_channels(
            list(channels.keys()),
            force=options.force,
            justdb=options.justdb,
            skip_packages=options.skip_packages,
            skip_channels=options.skip_channels,
            skip_kickstart_trees=options.skip_kickstart_trees,
            just_kickstart_trees=options.just_kickstart_trees,
        )
    except:
        rhnSQL.rollback()
        raise
    rhnSQL.commit()
    if options.unsubscribe:
        __applyChannelState(affected_minions, options.username, options.password)
    releaseLOCK()
    return 0


# pylint: disable-next=invalid-name
def releaseLOCK():
    # pylint: disable-next=global-variable-not-assigned
    global LOCK
    for lock in LOCK:
        lock.release()


# pylint: disable-next=invalid-name
def systemExit(code, msgs=None):
    if msgs:
        if type(msgs) not in [type([]), type(())]:
            msgs = (msgs,)
        for msg in msgs:
            sys.stderr.write(str(msg) + "\n")
    sys.exit(code)


if __name__ == "__main__":
    try:
        sys.exit(main() or 0)
    except KeyboardInterrupt:
        sys.stderr.write("\nUser interrupted process.\n")
        releaseLOCK()
        sys.exit(0)
    except UserError as error:
        # pylint: disable-next=consider-using-f-string
        systemExit(-1, "\n%s" % error)
    # pylint: disable-next=try-except-raise
    except SystemExit:
        # Normal exit
        raise
    # pylint: disable-next=broad-exception-caught
    except Exception:
        e = sys.exc_info()[1]
        releaseLOCK()
        # pylint: disable-next=consider-using-f-string
        sys.stderr.write("\nERROR: unhandled exception occurred: (%s).\n" % e)
        import traceback

        traceback.print_exc()
        sys.exit(-1)
