# copyright 2003-2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <https://www.gnu.org/licenses/>.
"""the cubicweb-ctl tool, based on logilab.common.clcommands to
provide a pluggable commands system.
"""
# *ctl module should limit the number of import to be imported as quickly as
# possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
# completion). So import locally in command helpers.
import os
import sys
import traceback
from os import listdir
from os.path import exists, join, isdir
from warnings import filterwarnings

from os import kill


from logilab.common.clcommands import CommandLine
from logilab.common.shellutils import ASK
from logilab.common.configuration import merge_options
from logilab.common.decorators import clear_cache

from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage, utils, repoapi
from cubicweb.cwconfig import (
    CubicWebConfiguration as cwcfg,
    instance_configuration,
)
from cubicweb.server import set_debug
from cubicweb.server.repository import Repository, get_cnxset
from cubicweb.toolsutils import Command, rm, create_dir, underline_title
from cubicweb.__pkginfo__ import version as cw_version

LOG_LEVELS = ("debug", "info", "warning", "error")
DBG_FLAGS = ("RQL", "SQL", "REPO", "HOOKS", "OPS", "SEC", "MORE", "ALL")

# don't check duplicated commands, it occurs when reloading site_cubicweb
CWCTL = CommandLine(
    "cubicweb-ctl",
    "The CubicWeb swiss-knife.",
    version=cw_version,
    check_duplicated_command=False,
)


def wait_process_end(pid, maxtry=10, waittime=1):
    """wait for a process to actually die"""
    import signal
    from time import sleep

    nbtry = 0
    while nbtry < maxtry:
        kill(pid, signal.SIGUSR1)
        nbtry += 1
        sleep(waittime)
    else:
        raise ExecutionError(f"can't kill process {pid}")


def list_instances(regdir):
    if isdir(regdir):
        return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
    else:
        return []


def detect_available_modes(templdir):
    modes = []
    for fname in ("schema", "schema.py"):
        if exists(join(templdir, fname)):
            modes.append("repository")
            break
    for fname in ("data", "views", "views.py"):
        if exists(join(templdir, fname)):
            modes.append("web ui")
            break
    return modes


def available_cube_names(cwcfg):
    """Return a list of available cube names, with 'cubicweb_' prefix dropped."""

    def drop_prefix(cube):
        prefix = "cubicweb_"
        if cube.startswith(prefix):
            cube = cube[len(prefix) :]
        return cube

    return [drop_prefix(cube) for cube in cwcfg.available_cubes()]


class InstanceCommand(Command):
    """base class for command taking one instance id as arguments"""

    arguments = "<instance>"

    # enforce having one instance
    min_args = 0
    max_args = 1

    options = (
        (
            "force",
            {
                "short": "f",
                "action": "store_true",
                "default": False,
                "help": "force command without asking confirmation",
            },
        ),
        (
            "pdb",
            {
                "action": "store_true",
                "default": False,
                "help": "launch pdb on exception",
            },
        ),
        (
            "loglevel",
            {
                "type": "choice",
                "default": None,
                "metavar": "<log level>",
                "choices": LOG_LEVELS,
                "short": "l",
                "help": "allow to specify log level for debugging (choices: %s)"
                % (", ".join(LOG_LEVELS)),
            },
        ),
        (
            "dbglevel",
            {
                "type": "multiple_choice",
                "metavar": "<debug level>",
                "default": None,
                "choices": DBG_FLAGS,
                "help": (
                    "Set the server debugging flags; you may choose several "
                    'values in %s; imply "debug" loglevel if loglevel is not set'
                    % (DBG_FLAGS,)
                ),
            },
        ),
    )
    actionverb = None

    @property
    def _cnx(self):
        return repoapi.get_repository(config=self.cwconfig).internal_cnx()

    def run(self, args):
        """run the <command>_method on each argument (a list of instance
        identifiers)
        """
        if not args:
            if "CW_INSTANCE" in os.environ:
                appid = os.environ["CW_INSTANCE"]
            else:
                raise BadCommandUsage("Error: instance id is missing")
        else:
            appid = args[0]

        cmdmeth = getattr(self, f"{self.name}_instance")

        traceback_ = None

        # debugmode=True is to force to have a StreamHandler used instead of
        # writting the logs into a file in /tmp
        self.cwconfig = cwcfg.config_for(appid, debugmode=True)

        # by default loglevel is 'error' but we keep the default value to None
        # because some subcommands (e.g: pyramid) can override the loglevel in
        # certain situations if it's not explicitly set by the user and we want
        # to detect that (the "None" case)
        if self["loglevel"] is None:
            # if no loglevel is set but dbglevel is here we want to set level to debug
            if self["dbglevel"]:
                init_cmdline_log_threshold(self.cwconfig, "debug")
            else:
                init_cmdline_log_threshold(self.cwconfig, "error")
        else:
            init_cmdline_log_threshold(self.cwconfig, self["loglevel"])

        if self["dbglevel"]:
            set_debug("|".join("DBG_" + x.upper() for x in self["dbglevel"]))

        try:
            status = cmdmeth(appid) or 0
        except (ExecutionError, ConfigurationError) as ex:
            # we need to do extract this information here for pdb since it is
            # now lost in python 3 once we exit the try/catch statement
            exception_type, exception, traceback_ = sys.exc_info()

            sys.stderr.write(f"instance {appid} not {self.actionverb}: {ex}\n")
            status = 4
        except Exception as ex:
            # idem
            exception_type, exception, traceback_ = sys.exc_info()

            traceback.print_exc()

            sys.stderr.write(f"instance {appid} not {self.actionverb}: {ex}\n")
            status = 8

        except (KeyboardInterrupt, SystemExit) as ex:
            # idem
            exception_type, exception, traceback_ = sys.exc_info()

            sys.stderr.write(f"{self.name} aborted\n")
            if isinstance(ex, KeyboardInterrupt):
                status = 2  # specific error code
            else:
                status = ex.code

        if status != 0 and self.config.pdb:
            pdb = utils.get_pdb()

            if traceback_ is not None:
                pdb.post_mortem(traceback_)
            else:
                print(
                    "WARNING: Could not access to the traceback because the command return "
                    "code is different than 0 but the command didn't raised an exception."
                )
                # we can't use "header=" of set_trace because ipdb doesn't supports it
                pdb.set_trace()

        sys.exit(status)


# base commands ###############################################################


class ListCommand(Command):
    """List configurations, cubes and instances.

    List available installed cubes and registered instances.

    If given, the optional argument allows to restrict listing only a category of items.
    """

    name = "list"
    arguments = "[all|cubes|instances]"
    options = (
        (
            "verbose",
            {"short": "v", "action": "store_true", "help": "display more information."},
        ),
    )

    def run(self, args):
        """run the command with its specific arguments"""
        if not args:
            mode = "all"
        elif len(args) == 1:
            mode = args[0]
        else:
            raise BadCommandUsage("Too many arguments")

        from cubicweb.migration import ConfigurationProblem

        if mode == "all":
            print(f"CubicWeb {cwcfg.cubicweb_version()} ({cwcfg.mode} mode)")
            print()

        if mode in ("all", "cubes"):
            cfgpb = ConfigurationProblem(cwcfg)
            try:
                cube_names = available_cube_names(cwcfg)
                namesize = max(len(x) for x in cube_names)
            except ConfigurationError as ex:
                print("No cubes available:", ex)
            except ValueError:
                print("No cubes available")
            else:
                print("Available cubes:")
                for cube in cube_names:
                    try:
                        tinfo = cwcfg.cube_pkginfo(cube)
                        tversion = tinfo.version
                        cfgpb.add_cube(cube, tversion)
                    except (ConfigurationError, AttributeError) as ex:
                        tinfo = None
                        tversion = f"[missing cube information: {ex}]"
                    print(f"* {cube.ljust(namesize)} {tversion}")
                    if self.config.verbose:
                        if tinfo:
                            descr = getattr(tinfo, "description", "")
                            if not descr:
                                descr = tinfo.__doc__
                            if descr:
                                print("    " + "    \n".join(descr.splitlines()))
                        modes = detect_available_modes(cwcfg.cube_dir(cube))
                        print(f"    available modes: {', '.join(modes)}")
            print()

        if mode in ("all", "instances"):
            try:
                regdir = cwcfg.instances_dir()
            except ConfigurationError as ex:
                print("No instance available:", ex)
                print()
                return
            instances = list_instances(regdir)
            if instances:
                print(f"Available instances ({regdir}):")
                for appid in instances:
                    print(f"* {appid}")
            else:
                print(f"No instance available in {regdir}")
            print()

        if mode == "all":
            # configuration management problem solving
            cfgpb.solve()
            if cfgpb.warnings:
                print("Warnings:\n", "\n".join("* " + txt for txt in cfgpb.warnings))
            if cfgpb.errors:
                print("Errors:")
                for op, cube, version, src in cfgpb.errors:
                    if op == "add":
                        print("* cube", cube, end=" ")
                        if version:
                            print(" version", version, end=" ")
                        print(f"is not installed, but required by {src}")
                    else:
                        print(
                            "* cube %s version %s is installed, but version %s is required by %s"
                            % (cube, cfgpb.cubes[cube], version, src)
                        )


def check_options_consistency(config):
    if config.automatic and config.config_level > 0:
        raise BadCommandUsage(
            "--automatic and --config-level should not be " "used together"
        )


class CreateInstanceCommand(Command):
    """Create an instance from a cube. This is a unified
    command which can handle web / server / all-in-one installation
    according to available parts of the software library and of the
    desired cube.

    <cube>
      the name of cube to use (list available cube names using
      the "list" command). You can use several cubes by separating
      them using comma (e.g. 'jpl,email')
    <instance>
      an identifier for the instance to create
    """

    name = "create"
    arguments = "<cube> <instance>"
    min_args = max_args = 2
    options = (
        (
            "automatic",
            {
                "short": "a",
                "action": "store_true",
                "default": False,
                "help": (
                    "automatic mode: never ask and use default answer to every "
                    "question. this may require that your login match a database super "
                    "user (allowed to create database & all)."
                ),
            },
        ),
        (
            "config-level",
            {
                "short": "l",
                "type": "int",
                "metavar": "<level>",
                "default": 0,
                "help": (
                    "configuration level (0..2): 0 will ask for essential "
                    "configuration parameters only while 2 will ask for all parameters"
                ),
            },
        ),
        (
            "no-db-create",
            {
                "short": "S",
                "action": "store_true",
                "default": False,
                "help": "stop after creation and do not continue with db-create",
            },
        ),
    )

    def run(self, args):
        """run the command with its specific arguments"""
        from logilab.common.textutils import splitstrip

        check_options_consistency(self.config)
        configname = "all-in-one"
        cubes, appid = args
        cubes = splitstrip(cubes)
        # get the configuration and helper
        config = cwcfg.config_for(appid, configname, creating=True)
        cubes = config.expand_cubes(cubes)
        config.init_cubes(cubes)
        helper = self.config_helper(config)
        # check the cube exists
        try:
            templdirs = [cwcfg.cube_dir(cube) for cube in cubes]
        except ConfigurationError as ex:
            print(ex)
            print("\navailable cubes:", end=" ")
            print(", ".join(available_cube_names(cwcfg)))
            return
        # create the registry directory for this instance
        print("\n" + underline_title(f"Creating the instance {appid}"))
        create_dir(config.apphome)
        # cubicweb-ctl configuration
        if not self.config.automatic:
            print(
                "\n" + underline_title(f"Configuring the instance ({configname}.conf)")
            )
            config.input_config("main", self.config.config_level)
        # configuration'specific stuff
        print()
        helper.bootstrap(cubes, self.config.automatic, self.config.config_level)
        # input for cubes specific options
        if not self.config.automatic:
            sections = set(
                sect.lower()
                for sect, opt, odict in config.all_options()
                if "type" in odict and odict.get("level", 0) <= self.config.config_level
            )
            for section in sections:
                if section not in ("main", "email", "web"):
                    print("\n" + underline_title(f"{section} options"))
                    config.input_config(section, self.config.config_level)
        # write down configuration
        config.save()
        print(f"-> generated config {config.main_config_file()}")
        # handle i18n files structure
        # in the first cube given
        from cubicweb import i18n

        langs = [
            lang for lang, _ in i18n.available_catalogs(join(templdirs[0], "i18n"))
        ]
        errors = config.i18ncompile(langs)
        if errors:
            print("\n".join(errors))
            if self.config.automatic or not ASK.confirm(
                "error while compiling message catalogs, " "continue anyway ?"
            ):
                print("creation not completed")
                return
        # create the additional data directory for this instance
        if config.appdatahome != config.apphome:  # true in dev mode
            create_dir(config.appdatahome)
        create_dir(join(config.appdatahome, "backup"))
        if config["uid"]:
            from logilab.common.shellutils import chown

            # this directory should be owned by the uid of the server process
            print(f"set {config['uid']} as owner of the data directory")
            chown(config.appdatahome, config["uid"])
        print(f"\n-> creation done for {repr(config.apphome)[1:-1]}\n")
        if not self.config.no_db_create:
            helper.postcreate(self.config.automatic, self.config.config_level)


class DeleteInstanceCommand(Command):
    """Delete an instance. Will remove instance's files and
    unregister it.
    """

    name = "delete"
    arguments = "<instance>"
    min_args = max_args = 1

    def run(self, args):
        """run the command with its specific arguments"""
        appid = args[0]
        config = cwcfg.config_for(appid)
        helper = self.config_helper(config, required=False)
        if helper:
            helper.cleanup()
        # remove home
        rm(config.apphome)
        # remove instance data directory
        try:
            rm(config.appdatahome)
        except OSError as ex:
            import errno

            if ex.errno != errno.ENOENT:
                raise
        print(f"-> instance {appid} deleted.")


# instance commands ########################################################


def init_cmdline_log_threshold(config, loglevel):
    if loglevel is not None:
        config.global_set_option("log-threshold", loglevel.upper())
        config.init_log(config["log-threshold"], force=True)


class UpgradeInstanceCommand(InstanceCommand):
    """Upgrade an instance after cubicweb and/or component(s) upgrade.

    For repository update, you will be prompted for a login / password to use
    to connect to the system database.  For some upgrades, the given user
    should have create or alter table permissions.

    <instance>
      identifier of the instance to upgrade.
    """

    name = "upgrade"
    actionverb = "upgraded"
    options = InstanceCommand.options + (
        (
            "force-cube-version",
            {
                "short": "t",
                "type": "named",
                "metavar": "cube1:X.Y.Z,cube2:X.Y.Z",
                "default": None,
                "help": "force migration from the indicated version for the specified cube(s).",
            },
        ),
        (
            "force-cubicweb-version",
            {
                "short": "e",
                "type": "string",
                "metavar": "X.Y.Z",
                "default": None,
                "help": "force migration from the indicated cubicweb version.",
            },
        ),
        (
            "fs-only",
            {
                "short": "s",
                "action": "store_true",
                "default": False,
                "help": "only upgrade files on the file system, not the database.",
            },
        ),
        (
            "no-config-update",
            {
                "short": "C",
                "action": "store_true",
                "default": False,
                "help": "do NOT update config file if set.",
            },
        ),
        (
            "verbosity",
            {
                "short": "v",
                "type": "int",
                "metavar": "<0..2>",
                "default": 1,
                "help": "0: no confirmation, 1: only main commands confirmed, 2 ask \
for everything.",
            },
        ),
        (
            "backup-db",
            {
                "short": "b",
                "type": "yn",
                "metavar": "<y or n>",
                "default": None,
                "help": (
                    "Backup the instance database before upgrade.\n"
                    "If the option is ommitted, confirmation will be ask."
                ),
            },
        ),
        (
            "ext-sources",
            {
                "short": "E",
                "type": "csv",
                "metavar": "<sources>",
                "default": None,
                "help": "For multisources instances, specify to which sources the \
repository should connect to for upgrading. When unspecified or 'migration' is \
given, appropriate sources for migration will be automatically selected \
(recommended). If 'all' is given, will connect to all defined sources.",
            },
        ),
    )

    def upgrade_instance(self, appid):
        print("\n" + underline_title(f"Upgrading the instance {appid}"))
        from logilab.common.changelog import Version

        config = cwcfg.config_for(appid)
        config.repairing = True  # notice we're not starting the server
        config.verbosity = self.config.verbosity
        set_sources_mode = getattr(config, "set_sources_mode", None)
        if set_sources_mode is not None:
            set_sources_mode(self.config.ext_sources or ("migration",))
        # get instance and installed versions for the server and the componants
        mih = config.migration_handler()
        repo = mih.repo
        vcconf = repo.get_versions()
        helper = self.config_helper(config, required=False)
        if self.config.force_cube_version:
            for cube, version in self.config.force_cube_version.items():
                vcconf[cube] = Version(version)
        toupgrade = []
        for cube in config.cubes():
            installedversion = config.cube_version(cube)
            try:
                applversion = vcconf[cube]
            except KeyError:
                config.error(f"no version information for {cube}")
                continue
            if installedversion > applversion:
                toupgrade.append((cube, applversion, installedversion))
        cubicwebversion = config.cubicweb_version()
        if self.config.force_cubicweb_version:
            applcubicwebversion = Version(self.config.force_cubicweb_version)
            vcconf["cubicweb"] = applcubicwebversion
        else:
            applcubicwebversion = vcconf.get("cubicweb")
        if cubicwebversion > applcubicwebversion:
            toupgrade.append(("cubicweb", applcubicwebversion, cubicwebversion))
        # run cubicweb/componants migration scripts
        if self.config.fs_only or toupgrade:
            for cube, fromversion, toversion in toupgrade:
                print(
                    f"-> migration needed from {fromversion} to {toversion} for {cube}"
                )
            with mih.cnx:
                with mih.cnx.security_enabled(False, False):
                    mih.migrate(vcconf, reversed(toupgrade), self.config)
            clear_cache(config, "instance_md5_version")
        else:
            print(f"-> no data migration needed for instance {appid}.")
        # rewrite main configuration file
        if not self.config.no_config_update:
            mih.rewrite_configuration()
        mih.shutdown()
        # handle i18n upgrade
        if not self.i18nupgrade(config):
            return
        print()
        if helper:
            helper.postupgrade(repo)
        print("-> instance migrated.")
        print()

    def i18nupgrade(self, config):
        # handle i18n upgrade:
        # * install new languages
        # * recompile catalogs
        # XXX search available language in the first cube given
        from cubicweb import i18n

        templdir = cwcfg.cube_dir(config.cubes()[0])
        langs = [lang for lang, _ in i18n.available_catalogs(join(templdir, "i18n"))]
        errors = config.i18ncompile(langs)
        if errors:
            print("\n".join(errors))
            if not ASK.confirm(
                "Error while compiling message catalogs, " "continue anyway?"
            ):
                print("-> migration not completed.")
                return False
        return True


class ListVersionsInstanceCommand(InstanceCommand):
    """List versions used by an instance.

    <instance>...
      identifiers of the instances to list versions for.
    """

    name = "versions"

    def versions_instance(self, appid):
        config = self.cwconfig
        # should not raise error if db versions don't match fs versions
        config.repairing = True
        # no need to load all appobjects and schema
        config.quick_start = True
        if hasattr(config, "set_sources_mode"):
            config.set_sources_mode(("migration",))
        config = instance_configuration(appid)
        # We need to initialize only a few things to have all cube versions.
        # So we can't use regular methods, since we only need a Repository
        # with a configuration, a connection and a schema.
        # Thus, we'll be able to list all cube versions of the instance,
        # even if some cubes aren't installed, which is impossible
        # if we simply use config.repository().get_versions().
        repo = Repository(config)
        repo.cnxsets = get_cnxset(repo.config, repo.system_source, bootstrap=True)
        repo.set_schema(repo.config.load_bootstrap_schema(), resetvreg=False)
        vcconf = repo.get_versions(checkversions=False)
        for key in sorted(vcconf):
            print(key + ": %s.%s.%s" % vcconf[key])


class ShellCommand(Command):
    """Run an interactive migration shell on an instance. This is a python shell
    with enhanced migration commands predefined in the namespace. An additional
    argument may be given corresponding to a file containing commands to execute
    in batch mode.

    By default it will connect to a local instance using an in memory
    connection, unless a URL to a running instance is specified.

    Arguments after bare "--" string will not be processed by the shell command
    You can use it to pass extra arguments to your script and expect for
    them in '__args__' afterwards.

    <instance>
      the identifier of the instance to connect.
    """

    name = "shell"
    arguments = "<instance> [batch command file(s)] [-- <script arguments>]"
    min_args = 1
    max_args = None
    options = merge_options(
        (
            (
                "system-only",
                {
                    "short": "S",
                    "action": "store_true",
                    "help": (
                        "only connect to the system source when the instance is "
                        "using multiple sources. You can't use this option and the "
                        "--ext-sources option at the same time."
                    ),
                    "group": "local",
                },
            ),
            (
                "ext-sources",
                {
                    "short": "E",
                    "type": "csv",
                    "metavar": "<sources>",
                    "help": "For multisources instances, specify to which sources the \
repository should connect to for upgrading. When unspecified or 'all' given, \
will connect to all defined sources. If 'migration' is given, appropriate \
sources for migration will be automatically selected.",
                    "group": "local",
                },
            ),
            (
                "force",
                {
                    "short": "f",
                    "action": "store_true",
                    "help": "don't check instance is up to date.",
                    "group": "local",
                },
            ),
        )
        + InstanceCommand.options
    )

    def _get_mih(self, appid):
        """returns migration context handler & shutdown function"""
        config = cwcfg.config_for(appid)
        if self.config.ext_sources:
            assert not self.config.system_only
            sources = self.config.ext_sources
        elif self.config.system_only:
            sources = ("system",)
        else:
            sources = ("all",)
        config.set_sources_mode(sources)
        config.repairing = self.config.force
        mih = config.migration_handler()
        return mih, lambda: mih.shutdown()

    def run(self, args):
        appuri = args.pop(0)
        mih, shutdown_callback = self._get_mih(appuri)
        try:
            with mih.cnx:
                with mih.cnx.security_enabled(False, False):
                    if args:
                        # use cmdline parser to access left/right attributes only
                        # remember that usage requires instance appid as first argument
                        scripts, args = (
                            self.cmdline_parser.largs[1:],
                            self.cmdline_parser.rargs,
                        )
                        for script in scripts:
                            mih.cmd_process_script(script, scriptargs=args)
                            mih.commit()
                    else:
                        mih.interactive_shell()
        finally:
            shutdown_callback()


class RecompileInstanceCatalogsCommand(InstanceCommand):
    """Recompile i18n catalogs for instances.

    <instance>
      identifier of the instance to consider.
    """

    name = "i18ninstance"

    def i18ninstance_instance(self, appid):
        """recompile instance's messages catalogs"""
        config = self.cwconfig
        config.quick_start = True  # notify this is not a regular start
        repo = config.repository()
        if config._cubes is None:
            # web only config
            config.init_cubes(repo.get_cubes())
        errors = config.i18ncompile()
        if errors:
            print("\n".join(errors))


class ListInstancesCommand(Command):
    """list available instances, useful for bash completion."""

    name = "listinstances"
    hidden = True

    def run(self, args):
        """run the command with its specific arguments"""
        regdir = cwcfg.instances_dir()
        for appid in sorted(listdir(regdir)):
            print(appid)


class ListCubesCommand(Command):
    """list available componants, useful for bash completion."""

    name = "listcubes"
    hidden = True

    def run(self, args):
        """run the command with its specific arguments"""
        for cube in cwcfg.available_cubes():
            print(cube)


class ConfigureInstanceCommand(InstanceCommand):
    """Configure instance.

    <instance>
      identifier of the instance to configure.
    """

    name = "configure"
    actionverb = "configured"

    options = merge_options(
        InstanceCommand.options
        + (
            (
                "param",
                {
                    "short": "p",
                    "type": "named",
                    "metavar": "key1:value1,key2:value2",
                    "default": None,
                    "help": "set <key> to <value> in configuration file.",
                },
            ),
        ),
    )

    def configure_instance(self, appid):
        if self.config.param is not None:
            appcfg = self.cwconfig
            for key, value in self.config.param.items():
                try:
                    appcfg.global_set_option(key, value)
                except KeyError:
                    raise ConfigurationError(
                        f'unknown configuration key "{key}" for mode {appcfg.name}'
                    )
            appcfg.save()


for cmdcls in (
    ListCommand,
    CreateInstanceCommand,
    DeleteInstanceCommand,
    UpgradeInstanceCommand,
    ListVersionsInstanceCommand,
    ShellCommand,
    RecompileInstanceCatalogsCommand,
    ListInstancesCommand,
    ListCubesCommand,
    ConfigureInstanceCommand,
):
    CWCTL.register(cmdcls)


def run(args=sys.argv[1:]):
    """command line tool"""
    filterwarnings("default", category=DeprecationWarning)
    cwcfg.load_cwctl_plugins()
    try:
        CWCTL.run(args)
    except ConfigurationError as err:
        print("ERROR: ", err)
        sys.exit(1)
    except ExecutionError as err:
        print(err)
        sys.exit(2)


if __name__ == "__main__":
    run()
