# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://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 <http://www.gnu.org/licenses/>.
"""the cubicweb-ctl tool, based on logilab.common.clcommands to
provide a pluggable commands system.
"""
from __future__ import print_function

# *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 sys
from warnings import warn, filterwarnings
from os import remove, listdir, system, pathsep
from os.path import exists, join, isdir, dirname, abspath

try:
    from os import kill, getpgid
except ImportError:
    def kill(*args):
        """win32 kill implementation"""
    def getpgid():
        """win32 getpgid implementation"""

from six.moves.urllib.parse import urlparse

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
from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CWDEV, CONFIGURATIONS
from cubicweb.toolsutils import Command, rm, create_dir, underline_title
from cubicweb.__pkginfo__ import version

# don't check duplicated commands, it occurs when reloading site_cubicweb
CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
                    version=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:
        try:
            kill(pid, signal.SIGUSR1)
        except (OSError, AttributeError):  # XXX win32
            break
        nbtry += 1
        sleep(waittime)
    else:
        raise ExecutionError('can\'t kill process %s' % 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


class InstanceCommand(Command):
    """base class for command taking 0 to n instance id as arguments
    (0 meaning all registered instances)
    """
    arguments = '[<instance>...]'
    options = (
        ("force",
         {'short': 'f', 'action' : 'store_true',
          'default': False,
          'help': 'force command without asking confirmation',
          }
         ),
        )
    actionverb = None

    def run(self, args):
        """run the <command>_method on each argument (a list of instance
        identifiers)
        """
        if not args:
            args = list_instances(cwcfg.instances_dir())
            try:
                askconfirm = not self.config.force
            except AttributeError:
                # no force option
                askconfirm = False
        else:
            askconfirm = False
        self.run_args(args, askconfirm)

    def run_args(self, args, askconfirm):
        status = 0
        for appid in args:
            if askconfirm:
                print('*'*72)
                if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
                    continue
            try:
                status = max(status, self.run_arg(appid))
            except (KeyboardInterrupt, SystemExit):
                sys.stderr.write('%s aborted\n' % self.name)
                return 2 # specific error code
        sys.exit(status)

    def run_arg(self, appid):
        cmdmeth = getattr(self, '%s_instance' % self.name)
        try:
            status = cmdmeth(appid)
        except (ExecutionError, ConfigurationError) as ex:
            sys.stderr.write('instance %s not %s: %s\n' % (
                    appid, self.actionverb, ex))
            status = 4
        except Exception as ex:
            import traceback
            traceback.print_exc()
            sys.stderr.write('instance %s not %s: %s\n' % (
                    appid, self.actionverb, ex))
            status = 8
        return status

class InstanceCommandFork(InstanceCommand):
    """Same as `InstanceCommand`, but command is forked in a new environment
    for each argument
    """

    def run_args(self, args, askconfirm):
        if len(args) > 1:
            forkcmd = ' '.join(w for w in sys.argv if not w in args)
        else:
            forkcmd = None
        for appid in args:
            if askconfirm:
                print('*'*72)
                if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
                    continue
            if forkcmd:
                status = system('%s %s' % (forkcmd, appid))
                if status:
                    print('%s exited with status %s' % (forkcmd, status))
            else:
                self.run_arg(appid)


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

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

    List available configurations, installed cubes, and registered instances.

    If given, the optional argument allows to restrict listing only a category of items.
    """
    name = 'list'
    arguments = '[all|cubes|configurations|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('CubicWeb %s (%s mode)' % (cwcfg.cubicweb_version(), cwcfg.mode))
            print()

        if mode in ('all', 'config', 'configurations'):
            cwcfg.load_available_configs()
            print('Available configurations:')
            for config in CONFIGURATIONS:
                print('*', config.name)
                for line in config.__doc__.splitlines():
                    line = line.strip()
                    if not line:
                        continue
                    print('   ', line)
            print()

        if mode in ('all', 'cubes'):
            cfgpb = ConfigurationProblem(cwcfg)
            try:
                cubesdir = pathsep.join(cwcfg.cubes_search_path())
                namesize = max(len(x) for x in cwcfg.available_cubes())
            except ConfigurationError as ex:
                print('No cubes available:', ex)
            except ValueError:
                print('No cubes available in %s' % cubesdir)
            else:
                print('Available cubes (%s):' % cubesdir)
                for cube in cwcfg.available_cubes():
                    try:
                        tinfo = cwcfg.cube_pkginfo(cube)
                        tversion = tinfo.version
                        cfgpb.add_cube(cube, tversion)
                    except (ConfigurationError, AttributeError) as ex:
                        tinfo = None
                        tversion = '[missing cube information: %s]' % ex
                    print('* %s %s' % (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('    available modes: %s' % ', '.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('Available instances (%s):' % regdir)
                for appid in instances:
                    modes = cwcfg.possible_configurations(appid)
                    if not modes:
                        print('* %s (BROKEN instance, no configuration found)' % appid)
                        continue
                    print('* %s (%s)' % (appid, ', '.join(modes)))
                    try:
                        config = cwcfg.config_for(appid, modes[0])
                    except Exception as exc:
                        print('    (BROKEN instance, %s)' % exc)
                        continue
            else:
                print('No instance available in %s' % 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('is not installed, but required by %s' % 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',
          }),
        ('config',
         {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
          'choices': ('all-in-one', 'repository', 'pyramid'),
          'default': 'all-in-one',
          'help': 'installation type, telling which part of an instance '
          'should be installed. You can list available configurations using the'
          ' "list" command. Default to "all-in-one", e.g. an installation '
          'embedding both the RQL repository and the web server.',
          }),
        ('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 = self.config.config
        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(cwcfg.available_cubes()))
            return
        # create the registry directory for this instance
        print('\n'+underline_title('Creating the instance %s' % appid))
        create_dir(config.apphome)
        # cubicweb-ctl configuration
        if not self.config.automatic:
            print('\n'+underline_title('Configuring the instance (%s.conf)'
                                       % configname))
            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('%s options' % section))
                    config.input_config(section, self.config.config_level)
        # write down configuration
        config.save()
        self._handle_win32(config, appid)
        print('-> generated config %s' % 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('set %s as owner of the data directory' % config['uid'])
            chown(config.appdatahome, config['uid'])
        print('\n-> creation done for %s\n' % repr(config.apphome)[1:-1])
        if not self.config.no_db_create:
            helper.postcreate(self.config.automatic, self.config.config_level)

    def _handle_win32(self, config, appid):
        if sys.platform != 'win32':
            return
        service_template = """
import sys
import win32serviceutil
sys.path.insert(0, r"%(CWPATH)s")

from cubicweb.etwist.service import CWService

classdict = {'_svc_name_': 'cubicweb-%(APPID)s',
             '_svc_display_name_': 'CubicWeb ' + '%(CNAME)s',
             'instance': '%(APPID)s'}
%(CNAME)sService = type('%(CNAME)sService', (CWService,), classdict)

if __name__ == '__main__':
    win32serviceutil.HandleCommandLine(%(CNAME)sService)
"""
        open(join(config.apphome, 'win32svc.py'), 'wb').write(
            service_template % {'APPID': appid,
                                'CNAME': appid.capitalize(),
                                'CWPATH': abspath(join(dirname(__file__), '..'))})


class DeleteInstanceCommand(Command):
    """Delete an instance. Will remove instance's files and
    unregister it.
    """
    name = 'delete'
    arguments = '<instance>'
    min_args = max_args = 1
    options = ()

    def run(self, args):
        """run the command with its specific arguments"""
        appid = args[0]
        configs = [cwcfg.config_for(appid, configname)
                   for configname in cwcfg.possible_configurations(appid)]
        if not configs:
            raise ExecutionError('unable to guess configuration for %s' % appid)
        for config in configs:
            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
        confignames = ', '.join([config.name for config in configs])
        print('-> instance %s (%s) deleted.' % (appid, confignames))


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

class StartInstanceCommand(InstanceCommandFork):
    """Start the given instances. If no instance is given, start them all.

    <instance>...
      identifiers of the instances to start. If no instance is
      given, start them all.
    """
    name = 'start'
    actionverb = 'started'
    options = (
        ("debug",
         {'short': 'D', 'action' : 'store_true',
          'help': 'start server in debug mode.'}),
        ("force",
         {'short': 'f', 'action' : 'store_true',
          'default': False,
          'help': 'start the instance even if it seems to be already \
running.'}),
        ('profile',
         {'short': 'P', 'type' : 'string', 'metavar': '<stat file>',
          'default': None,
          'help': 'profile code and use the specified file to store stats',
          }),
        ('loglevel',
         {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
          'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
          'help': 'debug if -D is set, error otherwise',
          }),
        ('param',
         {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
          'default': {},
          'help': 'override <key> configuration file option with <value>.',
         }),
       )

    def start_instance(self, appid):
        """start the instance's server"""
        try:
            import twisted  # noqa
        except ImportError:
            msg = (
                "Twisted is required by the 'start' command\n"
                "Either install it, or use one of the alternative commands:\n"
                "- '{ctl} pyramid {appid}'\n"
                "- '{ctl} wsgi {appid}'\n")
            raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
        config = cwcfg.config_for(appid, debugmode=self['debug'])
        # override config file values with cmdline options
        config.cmdline_options = self.config.param
        init_cmdline_log_threshold(config, self['loglevel'])
        if self['profile']:
            config.global_set_option('profile', self.config.profile)
        helper = self.config_helper(config, cmdname='start')
        pidf = config['pid-file']
        if exists(pidf) and not self['force']:
            msg = "%s seems to be running. Remove %s by hand if necessary or use \
the --force option."
            raise ExecutionError(msg % (appid, pidf))
        if helper.start_server(config) == 1:
            print('instance %s started' % appid)


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 StopInstanceCommand(InstanceCommand):
    """Stop the given instances.

    <instance>...
      identifiers of the instances to stop. If no instance is
      given, stop them all.
    """
    name = 'stop'
    actionverb = 'stopped'

    def stop_instance(self, appid):
        """stop the instance's server"""
        config = cwcfg.config_for(appid)
        helper = self.config_helper(config, cmdname='stop')
        helper.poststop() # do this anyway
        pidf = config['pid-file']
        if not exists(pidf):
            sys.stderr.write("%s doesn't exist.\n" % pidf)
            return
        import signal
        pid = int(open(pidf).read().strip())
        try:
            kill(pid, signal.SIGTERM)
        except Exception:
            sys.stderr.write("process %s seems already dead.\n" % pid)
        else:
            try:
                wait_process_end(pid)
            except ExecutionError as ex:
                sys.stderr.write('%s\ntrying SIGKILL\n' % ex)
                try:
                    kill(pid, signal.SIGKILL)
                except Exception:
                    # probably dead now
                    pass
                wait_process_end(pid)
        try:
            remove(pidf)
        except OSError:
            # already removed by twistd
            pass
        print('instance %s stopped' % appid)


class RestartInstanceCommand(StartInstanceCommand):
    """Restart the given instances.

    <instance>...
      identifiers of the instances to restart. If no instance is
      given, restart them all.
    """
    name = 'restart'
    actionverb = 'restarted'

    def restart_instance(self, appid):
        StopInstanceCommand(self.logger).stop_instance(appid)
        self.start_instance(appid)


class ReloadConfigurationCommand(RestartInstanceCommand):
    """Reload the given instances. This command is equivalent to a
    restart for now.

    <instance>...
      identifiers of the instances to reload. If no instance is
      given, reload them all.
    """
    name = 'reload'

    def reload_instance(self, appid):
        self.restart_instance(appid)


class StatusCommand(InstanceCommand):
    """Display status information about the given instances.

    <instance>...
      identifiers of the instances to status. If no instance is
      given, get status information about all registered instances.
    """
    name = 'status'
    options = ()

    @staticmethod
    def status_instance(appid):
        """print running status information for an instance"""
        status = 0
        for mode in cwcfg.possible_configurations(appid):
            config = cwcfg.config_for(appid, mode)
            print('[%s-%s]' % (appid, mode), end=' ')
            try:
                pidf = config['pid-file']
            except KeyError:
                print('buggy instance, pid file not specified')
                continue
            if not exists(pidf):
                print("doesn't seem to be running")
                status = 1
                continue
            pid = int(open(pidf).read().strip())
            # trick to guess whether or not the process is running
            try:
                getpgid(pid)
            except OSError:
                print("should be running with pid %s but the process can not be found" % pid)
                status = 1
                continue
            print("running with pid %s" % (pid))
        return status

class UpgradeInstanceCommand(InstanceCommandFork):
    """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>...
      identifiers of the instances to upgrade. If no instance is
      given, upgrade them all.
    """
    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.'}),

        ('nostartstop',
         {'short': 'n', 'action' : 'store_true',
          'default': False,
          'help': 'don\'t try to stop instance before migration and to restart it after.'}),

        ('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('Upgrading the instance %s' % appid))
        from logilab.common.changelog import Version
        config = cwcfg.config_for(appid)
        instance_running = exists(config['pid-file'])
        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('no version information for %s' % 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))
        # only stop once we're sure we have something to do
        if instance_running and not (CWDEV or self.config.nostartstop):
            StopInstanceCommand(self.logger).stop_instance(appid)
        # run cubicweb/componants migration scripts
        if self.config.fs_only or toupgrade:
            for cube, fromversion, toversion in toupgrade:
                print('-> migration needed from %s to %s for %s' % (fromversion, toversion, 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('-> no data migration needed for instance %s.' % appid)
        # rewrite main configuration file
        mih.rewrite_configuration()
        mih.shutdown()
        # handle i18n upgrade
        if not self.i18nupgrade(config):
            return
        print()
        if helper:
            helper.postupgrade(repo)
        print('-> instance migrated.')
        if instance_running and not (CWDEV or self.config.nostartstop):
            # restart instance through fork to get a proper environment, avoid
            # uicfg pb (and probably gettext catalogs, to check...)
            forkcmd = '%s start %s' % (sys.argv[0], appid)
            status = system(forkcmd)
            if status:
                print('%s exited with status %s' % (forkcmd, status))
        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 = cwcfg.config_for(appid)
        # 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',))
        vcconf = config.repository().get_versions()
        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
    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'
          }),

        ('repo-uri',
         {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
          'help': 'URI of the CubicWeb repository to connect to. URI can be \
a ZMQ URL or inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
directly give URI as instance id instead',
          'group': 'remote'
          }),
        )

    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)
        if self.config.repo_uri:
            warn('[3.16] --repo-uri option is deprecated, directly give the URI as instance id',
                 DeprecationWarning)
            if urlparse(self.config.repo_uri).scheme == 'inmemory':
                appuri = '%s/%s' % (self.config.repo_uri.rstrip('/'), appuri)

        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>...
      identifiers of the instances to consider. If no instance is
      given, recompile for all registered instances.
    """
    name = 'i18ninstance'

    @staticmethod
    def i18ninstance_instance(appid):
        """recompile instance's messages catalogs"""
        config = cwcfg.config_for(appid)
        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 = cwcfg.config_for(appid)
            for key, value in self.config.param.items():
                try:
                    appcfg.global_set_option(key, value)
                except KeyError:
                    raise ConfigurationError('unknown configuration key "%s" for mode %s' % (key, appcfg.name))
            appcfg.save()


# WSGI #########

WSGI_CHOICES = {}
try:
    from cubicweb.wsgi import server as stdlib_server
except ImportError:
    pass
else:
    WSGI_CHOICES['stdlib'] = stdlib_server
try:
    from cubicweb.wsgi import wz
except ImportError:
    pass
else:
    WSGI_CHOICES['werkzeug'] = wz
try:
    from cubicweb.wsgi import tnd
except ImportError:
    pass
else:
    WSGI_CHOICES['tornado'] = tnd


def wsgichoices():
    return tuple(WSGI_CHOICES)

if WSGI_CHOICES:
    class WSGIStartHandler(InstanceCommand):
        """Start an interactive wsgi server """
        name = 'wsgi'
        actionverb = 'started'
        arguments = '<instance>'

        @property
        def options(self):
            return (
                ("debug",
                 {'short': 'D', 'action': 'store_true',
                  'default': False,
                  'help': 'start server in debug mode.'}),
                ('method',
                 {'short': 'm',
                  'type': 'choice',
                  'metavar': '<method>',
                  'default': 'stdlib',
                  'choices': wsgichoices(),
                  'help': 'wsgi utility/method'}),
                ('loglevel',
                 {'short': 'l',
                  'type': 'choice',
                  'metavar': '<log level>',
                  'default': None,
                  'choices': ('debug', 'info', 'warning', 'error'),
                  'help': 'debug if -D is set, error otherwise',
              }),
            )

        def wsgi_instance(self, appid):
            config = cwcfg.config_for(appid, debugmode=self['debug'])
            init_cmdline_log_threshold(config, self['loglevel'])
            assert config.name == 'all-in-one'
            meth = self['method']
            server = WSGI_CHOICES[meth]
            return server.run(config)

    CWCTL.register(WSGIStartHandler)


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



def run(args):
    """command line tool"""
    import os
    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(sys.argv[1:])
