#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration helper for the ejabberd service
"""

import argparse
import json
import os
import pathlib
import shutil
import socket
import subprocess
from distutils.version import LooseVersion as LV

import ruamel.yaml

from plinth import action_utils

EJABBERD_CONFIG = '/etc/ejabberd/ejabberd.yml'
EJABBERD_BACKUP = '/var/log/ejabberd/ejabberd.dump'
EJABBERD_BACKUP_NEW = '/var/log/ejabberd/ejabberd_new.dump'
EJABBERD_ORIG_CERT = '/etc/ejabberd/ejabberd.pem'
IQDISC_DEPRECATED_VERSION = LV('18.03')
MOD_IRC_DEPRECATED_VERSION = LV('18.06')


def parse_arguments():
    """Return parsed command line arguments as dictionary"""
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')

    # Get configuration
    subparsers.add_parser('get-configuration',
                          help='Return the current configuration')

    # Preseed debconf values before packages are installed.
    pre_install = subparsers.add_parser(
        'pre-install',
        help='Preseed debconf values before packages are installed.')
    pre_install.add_argument(
        '--domainname',
        help='The domain name that will be used by the XMPP service.')

    # Setup ejabberd configuration
    setup = subparsers.add_parser('setup', help='Setup ejabberd configuration')
    setup.add_argument(
        '--domainname',
        help='The domain name that will be used by the XMPP service.')

    # Prepare ejabberd for hostname change
    pre_hostname_change = subparsers.add_parser(
        'pre-change-hostname', help='Prepare ejabberd for nodename change')
    pre_hostname_change.add_argument('--old-hostname',
                                     help='Previous hostname')
    pre_hostname_change.add_argument('--new-hostname', help='New hostname')

    # Update ejabberd nodename
    hostname_change = subparsers.add_parser('change-hostname',
                                            help='Update ejabberd nodename')
    hostname_change.add_argument('--old-hostname', help='Previous hostname')
    hostname_change.add_argument('--new-hostname', help='New hostname')

    # Add a domain name to ejabberd
    add_domain = subparsers.add_parser('add-domain',
                                       help='Add a domain name to ejabberd')
    add_domain.add_argument('--domainname', help='New domain name')

    # Switch/check Message Archive Management (MAM) in ejabberd config
    help_MAM = 'Switch or check Message Archive Management (MAM).'
    mam = subparsers.add_parser('mam', help=help_MAM)
    mam.add_argument('command', choices=('enable', 'disable', 'status'),
                     help=help_MAM)

    subparsers.required = True
    return parser.parse_args()


def subcommand_get_configuration(_):
    """Return the current configuration, specifically domains configured."""
    with open(EJABBERD_CONFIG, 'r') as file_handle:
        conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)

    print(json.dumps({'domains': conf['hosts']}))


def subcommand_pre_install(arguments):
    """Preseed debconf values before packages are installed."""
    domainname = arguments.domainname
    if not domainname:
        # If new domainname is blank, use hostname instead.
        domainname = socket.gethostname()

    action_utils.debconf_set_selections(
        ['ejabberd ejabberd/hostname string ' + domainname])


def subcommand_setup(arguments):
    """Enabled LDAP authentication"""
    with open(EJABBERD_CONFIG, 'r') as file_handle:
        conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)

    for listen_port in conf['listen']:
        if 'tls' in listen_port:
            listen_port['tls'] = False

    conf['auth_method'] = 'ldap'
    conf['ldap_servers'] = [
        ruamel.yaml.scalarstring.DoubleQuotedScalarString('localhost')
    ]
    conf['ldap_base'] = ruamel.yaml.scalarstring.DoubleQuotedScalarString(
        'ou=users,dc=thisbox')

    with open(EJABBERD_CONFIG, 'w') as file_handle:
        ruamel.yaml.round_trip_dump(conf, file_handle)

    upgrade_config(arguments.domainname)

    try:
        subprocess.check_output(['ejabberdctl', 'restart'])
    except subprocess.CalledProcessError as err:
        print('Failed to restart ejabberd with new configuration: %s', err)


def upgrade_config(domain):
    """Fix the config file by removing deprecated settings"""
    current_version = _get_version()
    if not current_version:
        print('Warning: Unable to get ejabberd version.')

    with open(EJABBERD_CONFIG, 'r') as file_handle:
        conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)

    # Check if `iqdisc` is present and remove it
    if 'mod_mam' in conf['modules'] and \
       (not current_version or current_version > IQDISC_DEPRECATED_VERSION):
        conf['modules']['mod_mam'].pop('iqdisc', None)

    # check if mod_irc is present in modules and remove it
    if 'mod_irc' in conf['modules'] and \
       (not current_version or current_version > MOD_IRC_DEPRECATED_VERSION):
        conf['modules'].pop('mod_irc')

    # Debian has a patch to configuration to change port 5443 to 5280 in
    # ejabberd package version 18.12. However, 5443 is the correct port to host
    # BOSH. So, change it back. In 19.x, this behavior has changed to use both
    # ports 5443 (for BOSH) and 5280 (for web administration).
    bosh_port = any((True for listen_port in conf['listen']
                     if listen_port['port'] == 5443))
    if not bosh_port:
        for listen_port in conf['listen']:
            if listen_port['port'] == 5280:
                listen_port['port'] = 5443

    cert_dir = pathlib.Path('/etc/ejabberd/letsencrypt') / domain
    cert_file = str(cert_dir / 'ejabberd.pem')
    cert_file = ruamel.yaml.scalarstring.DoubleQuotedScalarString(cert_file)
    conf['s2s_certfile'] = cert_file
    for listen_port in conf['listen']:
        if 'certfile' in listen_port:
            listen_port['certfile'] = cert_file

    # Write changes back to the file
    with open(EJABBERD_CONFIG, 'w') as file_handle:
        ruamel.yaml.round_trip_dump(conf, file_handle)


def subcommand_pre_change_hostname(arguments):
    """Prepare ejabberd for hostname change"""
    if not shutil.which('ejabberdctl'):
        print('ejabberdctl not found. Is ejabberd installed?')
        return

    old_hostname = arguments.old_hostname
    new_hostname = arguments.new_hostname

    subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP])
    try:
        subprocess.check_output([
            'ejabberdctl', 'mnesia-change-nodename',
            'ejabberd@' + old_hostname, 'ejabberd@' + new_hostname,
            EJABBERD_BACKUP, EJABBERD_BACKUP_NEW
        ])
        os.remove(EJABBERD_BACKUP)
    except subprocess.CalledProcessError as err:
        print('Failed to change hostname in ejabberd backup database: %s', err)


def subcommand_change_hostname(arguments):
    """Update ejabberd with new hostname"""
    if not shutil.which('ejabberdctl'):
        print('ejabberdctl not found. Is ejabberd installed?')
        return

    action_utils.service_stop('ejabberd')
    subprocess.call(['pkill', '-u', 'ejabberd'])

    # Make sure there aren't files in the Mnesia spool dir
    os.makedirs('/var/lib/ejabberd/oldfiles', exist_ok=True)
    subprocess.call('mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/',
                    shell=True)

    action_utils.service_start('ejabberd')

    # restore backup database
    if os.path.exists(EJABBERD_BACKUP_NEW):
        try:
            subprocess.check_output(
                ['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW])
            os.remove(EJABBERD_BACKUP_NEW)
        except subprocess.CalledProcessError as err:
            print('Failed to restore ejabberd backup database: %s', err)
    else:
        print('Could not load ejabberd backup database: %s not found' %
              EJABBERD_BACKUP_NEW)


def subcommand_add_domain(arguments):
    """Update ejabberd with new domainname"""
    if not shutil.which('ejabberdctl'):
        print('ejabberdctl not found. Is ejabberd installed?')
        return

    domainname = arguments.domainname

    # Add updated domainname to ejabberd hosts list.
    with open(EJABBERD_CONFIG, 'r') as file_handle:
        conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)

    conf['hosts'].append(
        ruamel.yaml.scalarstring.DoubleQuotedScalarString(domainname))

    conf['hosts'] = list(set(conf['hosts']))

    with open(EJABBERD_CONFIG, 'w') as file_handle:
        ruamel.yaml.round_trip_dump(conf, file_handle)


def subcommand_mam(argument):
    """Enable, disable, or get status of Message Archive Management (MAM)."""

    with open(EJABBERD_CONFIG, 'r') as file_handle:
        conf = ruamel.yaml.round_trip_load(file_handle, preserve_quotes=True)

    if 'modules' not in conf:
        print('Found no "modules" entry in ejabberd configuration file.')
        return

    if argument.command == 'status':
        if 'mod_mam' in conf['modules']:
            print('enabled')
            return
        else:
            print('disabled')
            return

    if argument.command == 'enable':
        # Explicitly set the recommended / default settings for mod_mam,
        # see https://docs.ejabberd.im/admin/configuration/#mod-mam.
        settings_mod_mam = {
            'mod_mam': {
                'db_type':
                    'mnesia',  # default is 'mnesia' (w/o set default_db)
                'default': 'never',  # policy, default 'never'
                'request_activates_archiving': False,  # default False
                'assume_mam_usage': False,  # for non-ack'd msgs, default False
                'cache_size': 1000,  # default is 1000 items
                'cache_life_time': 3600  # default is 3600 seconds = 1h
            }
        }
        conf['modules'].update(settings_mod_mam)
    elif argument.command == 'disable':
        # disable modules by erasing from config file
        if 'mod_mam' in conf['modules']:
            conf['modules'].pop('mod_mam')
    else:
        print("Unknown command: %s" % argument.command)
        return

    with open(EJABBERD_CONFIG, 'w') as file_handle:
        ruamel.yaml.round_trip_dump(conf, file_handle)

    if action_utils.service_is_running('ejabberd'):
        subprocess.call(['ejabberdctl', 'reload_config'])


def _get_version():
    """ Get the current ejabberd version """
    try:
        output = subprocess.check_output(['ejabberdctl',
                                          'status']).decode('utf-8')
    except subprocess.CalledProcessError:
        return None

    version_info = output.strip().split('\n')[-1].split()
    if version_info:
        version = str(version_info[1])
        return LV(version)
    return None


def main():
    """Parse arguments and perform all duties"""
    arguments = parse_arguments()

    subcommand = arguments.subcommand.replace('-', '_')
    subcommand_method = globals()['subcommand_' + subcommand]
    subcommand_method(arguments)


if __name__ == '__main__':
    main()
