# This file is part of Indico.
# Copyright (C) 2002 - 2026 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

import os
import re
import shutil
import socket
import subprocess
import sys
from importlib.metadata import entry_points
from operator import attrgetter
from pathlib import Path
from smtplib import SMTP
from urllib.parse import urlsplit

import click
from click import wrap_text
from flask.helpers import get_root_path
from packaging.specifiers import SpecifierSet
from packaging.version import Version
from prompt_toolkit import prompt
from prompt_toolkit.completion import PathCompleter, WordCompleter
from prompt_toolkit.styles import Style
from pytz import all_timezones, common_timezones
from redis import RedisError, StrictRedis
from sqlalchemy import create_engine
from sqlalchemy.exc import OperationalError
from sqlalchemy.pool import NullPool
from terminaltables import AsciiTable

import indico
from indico.core.db.sqlalchemy.util.models import import_all_models
from indico.util.console import cformat
from indico.util.string import validate_email


def _echo(msg=''):
    click.echo(msg, err=True)


def _warn(msg):
    msg = wrap_text(msg)
    click.echo(click.style(msg, fg='yellow'), err=True)


def _error(msg):
    msg = wrap_text(msg)
    click.echo(click.style(msg, fg='red', bold=True), err=True)


def _prompt_abort():
    _confirm('Continue anyway?', abort=True)


def _copy(src, dst, force=False):
    if not force and os.path.exists(dst):
        _echo(cformat('%{yellow!}{}%{reset}%{yellow} already exists; not copying %{yellow!}{}')
              .format(dst, src))
        return
    _echo(cformat('%{green}Copying %{green!}{}%{reset}%{green} -> %{green!}{}').format(src, dst))
    shutil.copy(src, dst)


def _link(src, dst):
    _echo(cformat('%{cyan}Linking %{cyan!}{}%{reset}%{cyan} -> %{cyan!}{}').format(dst, src))
    if os.path.exists(dst) or os.path.islink(dst):
        os.unlink(dst)
    os.symlink(src, dst)


def _get_dirs(target_dir):
    if not os.path.isdir(target_dir):
        _echo(cformat('%{red}Directory not found:%{red!} {}').format(target_dir))
        sys.exit(1)
    return get_root_path('indico'), os.path.abspath(target_dir)


PROMPT_TOOLKIT_STYLE = Style.from_dict({
    'help': '#aaaaaa',
    'prompt': '#5f87ff',
    'default': '#dfafff',
    'bracket': '#ffffff',
    'colon': '#ffffff',
    '': '#aaffaa',  # user input
})


def _prompt(message, default='', path=False, list_=None, required=True, validate=None, allow_invalid=False,
            password=False, help=None):
    def _get_prompt_tokens():
        rv = [
            ('class:prompt', message),
            ('class:colon', ': '),
        ]
        if first and help:
            rv.insert(0, ('class:help', wrap_text(help) + '\n'))
        return rv

    completer = None
    if path:
        completer = PathCompleter(only_directories=True, expanduser=True)
    elif list_:
        completer = WordCompleter(sorted(list_), ignore_case=True, sentence=True,
                                  meta_dict=(list_ if isinstance(list_, dict) else None))
        if validate is None:
            validate = list_.__contains__

    first = True
    while True:
        try:
            rv = prompt(_get_prompt_tokens(), default=default, is_password=password,
                        completer=completer, style=PROMPT_TOOLKIT_STYLE)
        except (EOFError, KeyboardInterrupt):
            sys.exit(1)
        # pasting a multiline string works even with multiline disabled :(
        rv = rv.replace('\n', ' ').strip()
        if not rv:
            if not required:
                break
        else:
            if path:
                rv = os.path.abspath(os.path.expanduser(rv))
            if validate is None or validate(rv):
                break
            if allow_invalid and _confirm('Keep this value anyway?'):
                break
            default = rv
        first = False
    return rv


def _confirm(message, default=False, abort=False, help=None):
    def _get_prompt_tokens():
        rv = [
            ('class:prompt', message),
            ('class:bracket', ' ['),
            ('class:default', 'Y/n' if default else 'y/N'),
            ('class:bracket', ']'),
            ('class:colon', ': '),
        ]
        if first and help:
            rv.insert(0, ('class:help', wrap_text(help) + '\n'))
        return rv

    first = True
    while True:
        try:
            rv = prompt(_get_prompt_tokens(),
                        completer=WordCompleter(['yes', 'no'], ignore_case=True, sentence=True),
                        style=PROMPT_TOOLKIT_STYLE)
        except (EOFError, KeyboardInterrupt):
            sys.exit(1)
        first = False
        rv = rv.replace('\n', ' ').strip().lower()
        if rv in ('y', 'yes', '1', 'true'):
            rv = True
        elif rv in ('n', 'no', '0', 'false'):
            rv = False
        elif not rv:
            rv = default
        else:
            _warn('Invalid input, enter Y or N')
            continue
        break
    if abort and not rv:
        raise click.Abort
    return rv


@click.group()
def cli():
    """This script helps with the initial steps of installing Indico."""


@cli.command()
def list_plugins():
    """List the available Indico plugins."""
    import_all_models()
    table_data = [['Name', 'Title']]
    for ep in sorted(entry_points(group='indico.plugins'), key=attrgetter('name')):
        plugin = ep.load()
        table_data.append([ep.name, plugin.title])
    table = AsciiTable(table_data, cformat('%{white!}Available Plugins%{reset}'))
    click.echo(table.table)


@cli.command()
@click.argument('target_dir')
def create_symlinks(target_dir):
    """Create useful symlinks to run Indico from a webserver.

    This lets you use static paths for the WSGI file and the htdocs
    folder so you do not need to update your webserver config when
    updating Indico.
    """
    root_dir, target_dir = _get_dirs(target_dir)
    _link(os.path.join(root_dir, 'web', 'static'), os.path.join(target_dir, 'static'))
    _copy(os.path.join(root_dir, 'web', 'indico.wsgi'), os.path.join(target_dir, 'indico.wsgi'), force=True)


@cli.command()
@click.argument('target_dir')
def create_logging_config(target_dir):
    """Create the default logging config file for Indico.

    If a file already exists it is left untouched. This command is
    usually only used when doing a fresh indico installation when
    not using the setup wizard.
    """
    root_dir, target_dir = _get_dirs(target_dir)
    _copy(os.path.normpath(os.path.join(root_dir, 'logging.yaml.sample')), os.path.join(target_dir, 'logging.yaml'))


@cli.command()
@click.option('--check', is_flag=True, help='Only check for updates but do not actually update.')
@click.option('--no-pyenv-update', is_flag=True, help='Do not update pyenv')
@click.option('--local', is_flag=True,
              help='Set the local pyenv version instead of the global one. Do not use this in a standard Indico '
                   'deployment; it is only meant for development where you do not have a dedicated indico user.')
@click.option('--force-version',
              help='Force using a custom python version, regardless of the preferred version this Indico version has. '
                   'Only use this when explicitly asked to, either by a developer or in the upgrade instructions.')
@click.option('--venv', required=True, metavar='PATH', envvar='VIRTUAL_ENV',
              type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
              help='The path of the Python venv; defaults to the active one.')
def upgrade_python(check, no_pyenv_update, local, force_version, venv):
    """Upgrade the Python version used in the Indico virtualenv.

    When using `--check`, only Python versions currently available in pyenv will
    be used; make sure to `pyenv update` first (this is done automatically when
    you run this command without `--check`).

    Be careful when using `--force-version` with a custom version; you may end up
    using an unsupported version where this command will no longer work.
    """
    if not check and not no_pyenv_update:
        click.echo('updating pyenv')
        proc = subprocess.run(['pyenv', 'update'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        if proc.returncode:
            click.echo(proc.stdout)
            click.confirm('pyenv update failed - continue anyway?', abort=True)

    pyenv_mode = 'local' if local else 'global'
    pyenv_version_list = subprocess.run(['pyenv', 'install', '--list'], capture_output=True,
                                        encoding='utf-8').stdout.splitlines()
    pyenv_versions = [Version(x) for line in pyenv_version_list
                      if (x := line.strip()) and re.match(r'^\d+\.\d+\.\d+$', x)]
    preferred_version_spec = SpecifierSet(indico.PREFERRED_PYTHON_VERSION_SPEC)
    current_version = Version('.'.join(map(str, sys.version_info[:3])))

    if force_version:
        preferred_version = Version(force_version)
        if preferred_version not in preferred_version_spec:
            click.echo(f'Warning: {preferred_version} is not within {preferred_version_spec} spec')
            click.confirm('Continue anyway?', abort=True)
    else:
        if not (available_versions := preferred_version_spec.filter(pyenv_versions)):
            click.echo(f'Found no qualifying versions for {preferred_version_spec}')
            sys.exit(1)
        preferred_version = max(available_versions)

    if current_version == preferred_version:
        click.echo(f'Already running on preferred version ({preferred_version})')
        sys.exit(0)

    if (preferred_version.major, preferred_version.minor) != (current_version.major, current_version.minor):
        old = f'{current_version.major}.{current_version.minor}'
        new = f'{preferred_version.major}.{preferred_version.minor}'
        click.echo(f'WARNING: You are upgrading from {old} to {new}. This upgrade CANNOT be done in-place.')
        click.echo('You will need to `pip install indico` (and any other packages such as plugins)\n'
                   'again after this upgrade! Do not perform this update, unless you are updating to\n'
                   'a new major Indico release which is documented to require this Python version!')
        click.confirm('Continue?', abort=True)

    pyenv_local_version = Version(subprocess.run(['pyenv', 'version-name'], capture_output=True,
                                                 encoding='utf-8').stdout.strip())
    if pyenv_local_version != preferred_version:
        click.echo(f'Currently selected version {pyenv_local_version} does not match '
                   f'preferred version {preferred_version}')
        if not check:
            click.echo(f'Installing python {preferred_version} (may take some time)')
            proc = subprocess.run(['pyenv', 'install', '--skip-existing', '--verbose', str(preferred_version)],
                                  stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            if proc.returncode:
                click.echo(proc.stdout)
                click.echo(f'Installing python {preferred_version} update failed')
                sys.exit(1)
            click.echo(f'Setting {pyenv_mode} pyenv version to {preferred_version}')
            subprocess.run(['pyenv', pyenv_mode, str(preferred_version)], check=True)
        else:
            click.echo(f'Would set {pyenv_mode} pyenv version to {preferred_version} (skipping due to --check)')

    # Upgrade the virtualenv in-place
    from . import python_upgrader
    args = [venv]
    if check:
        args.append('--check')
    # we need to run the upgrade script in an isolated environment to avoid any
    # of the currently-installed packages or PYTHONPATH info leaking into the
    # environment in case the venv builder needs to upgrade pip
    subprocess.run([sys.executable, '-I', python_upgrader.__file__, *args], check=True)


@cli.command()
@click.option('--dev', is_flag=True)
def wizard(dev):
    """Run a setup wizard to configure Indico from scratch."""
    SetupWizard().run(dev=dev)


class SetupWizard:
    def __init__(self):
        self._missing_dirs = set()
        self.root_path = None
        self.data_root_path = None
        self.config_dir_path = None
        self.config_path = None
        self.indico_url = None
        self.db_uri = None
        self.redis_uri_celery = None
        self.redis_uri_cache = None
        self.contact_email = None
        self.admin_email = None
        self.noreply_email = None
        self.smtp_host = None
        self.smtp_port = None
        self.smtp_user = None
        self.smtp_password = None
        self.default_locale = None
        self.default_timezone = None
        self.rb_active = None
        self.system_notices = True
        self.public_user_search = None

    def run(self, dev):
        self._check_root()
        self._check_venv()
        self._prompt_root_path(dev=dev)
        self.config_dir_path = Path(get_root_path('indico') if dev else os.path.join(self.root_path, 'etc'))
        self.config_path = self.config_dir_path / 'indico.conf'
        self._check_configured()
        self._check_directories(dev=dev)
        self._prompt_indico_url(dev=dev)
        self._prompt_db_uri()
        self._prompt_redis_uris()
        self._prompt_emails()
        self._prompt_smtp_data(dev=dev)
        self._prompt_defaults()
        self._prompt_misc()
        self._setup(dev=dev)

    def _check_root(self):
        if os.getuid() == 0 or os.geteuid() == 0:
            _error('Indico must not be installed as root.')
            _error('Please create a user (e.g. `indico`) and run this script again using that user.')
            sys.exit(1)

    def _check_venv(self):
        if sys.prefix == sys.base_prefix:
            _warn('It looks like you are not using a virtualenv. This is unsupported and strongly discouraged.')
            _prompt_abort()

    def _prompt_root_path(self, dev=False):
        if dev:
            default_root = os.getcwd()
            if os.path.exists(os.path.join(default_root, '.git')):
                # based on the setup guide the most likely location to run
                # this commanad from is `~/dev/indico/src`, in which case
                # we want to go up a level since the data dir should not be
                # created inside the source directory
                default_root = os.path.dirname(default_root)
        else:
            default_root = '/opt/indico'
        self.root_path = _prompt('Indico root path', default=default_root, path=True,
                                 help='Enter the base directory where Indico will be installed.')
        # The root needs to exist (ideally created during `useradd`)
        if not os.path.isdir(self.root_path):
            _error('The specified root path path does not exist.')
            raise click.Abort
        # There is no harm in having the virtualenv somewhere else but it
        # is cleaner to keep everything within the root path
        if not sys.prefix.startswith(self.root_path + '/'):
            _warn(f'It is recommended to have the virtualenv inside the root directory, e.g. {self.root_path}/.venv')
            _warn(f'The virtualenv is currently located in {sys.prefix}')
            _prompt_abort()
        self.data_root_path = os.path.join(self.root_path, 'data') if dev else self.root_path

    def _check_configured(self):
        # Bail out early if indico is already configured
        if self.config_path.exists():
            _error(
                f'Config file already exists. If you really want to run this wizard again, delete {self.config_path}'
            )
            raise click.Abort

    def _check_directories(self, dev=False):
        dirs = ['archive', 'cache', 'log', 'tmp']
        if not dev:
            dirs += ['etc', 'web']

        _echo('Indico will use the following directories')
        has_existing = False
        for name in dirs:
            path = os.path.join(self.data_root_path, name)
            exists_suffix = ''
            if os.path.exists(path):
                has_existing = True
                exists_suffix = cformat(' %{yellow!}*%{reset}')
            else:
                self._missing_dirs.add(path)
            _echo(cformat('  %{blue!}{}%{reset}').format(path) + exists_suffix)
        if has_existing:
            _warn('The directories marked with a * already exist.  We strongly recommend installing Indico in a new '
                  'directory that contains nothing but the Python virtualenv.  If you are upgrading from Indico v1.2, '
                  'please move its data to a different location.')
            _prompt_abort()

    def _prompt_indico_url(self, dev=False):
        def _check_url(url):
            data = urlsplit(url)
            if not data.scheme or not data.hostname:
                _warn('Invalid URL: scheme/host missing')
                return False
            elif data.fragment or data.query or '@' in data.netloc:
                _warn('Invalid URL: must not contain fragment, query string or username/password')
                return False
            elif not dev and data.scheme != 'https':
                _warn('Unless this is a test instance it is HIGHLY RECOMMENDED to use an https:// URL')
                return False
            elif data.path not in ('', '/'):
                _warn('It is recommended to run Indico on a subdomain instead of a subdirectory')
                _warn('Unless you built your own indico package for the specified path, '
                      'Indico will not work correctly!')
                return False
            return True

        default_url = ('http://127.0.0.1:8000' if dev else f'https://{socket.getfqdn()}')
        url = _prompt('Indico URL', validate=_check_url, allow_invalid=True,
                      default=default_url,
                      help='Indico needs to know the URL through which it is accessible. '
                           'We strongly recommend using an https:// URL and a subdomain, e.g. '
                           'https://indico.yourdomain.com')
        self.indico_url = url.rstrip('/')

    def _prompt_db_uri(self):
        def _check_postgres(uri, silent=False):
            try:
                engine = create_engine(uri, connect_args={'connect_timeout': 3}, poolclass=NullPool)
                engine.connect().close()
            except OperationalError as exc:
                if not silent:
                    _warn('Invalid database URI: ' + str(exc.orig).strip())
                return False
            else:
                return True

        # Local postgres instance running and accessible?
        if _check_postgres('postgresql:///template1', silent=True):
            default = 'postgresql:///indico'
        else:
            default = 'postgresql://user:pass@host/indico'
        self.db_uri = _prompt('PostgreSQL database URI', default=default,
                              validate=_check_postgres, allow_invalid=True,
                              help='Enter the SQLAlchemy connection string to connect to your PostgreSQL database. '
                                   'For a remote database, use postgresql://user:pass@host/dbname')

    def _prompt_redis_uris(self):
        def _check_redis(uri):
            client = StrictRedis.from_url(uri)
            client.connection_pool.connection_kwargs['socket_timeout'] = 3
            try:
                client.ping()
            except RedisError as exc:
                _warn('Invalid redis URI: ' + str(exc))
                return False
            else:
                return True

        def _increase_redis_dbnum(uri):
            return re.sub(r'(?<=/)(\d+)$', lambda m: str(int(m.group(1)) + 1), uri)

        self.redis_uri_celery = _prompt('Redis URI (celery)', default='redis://127.0.0.1:6379/0',
                                        validate=_check_redis, allow_invalid=True,
                                        help='Indico uses Redis to manage scheduled/background tasks. '
                                             'Enter the URL to your Redis server. '
                                             'Running one locally is usually the easiest option.')
        self.redis_uri_cache = _prompt('Redis URI (cache)', default=_increase_redis_dbnum(self.redis_uri_celery),
                                       validate=_check_redis, allow_invalid=True,
                                       help='Redis is also used for caching. We recommend using a separate redis '
                                            'database, which allows you to clear the cache without affecting celery')

    def _prompt_emails(self):
        def _check_email(email):
            if validate_email(email):
                return True
            _warn('Invalid email address')
            return False

        def _get_noreply_email(email):
            return 'noreply@' + email.split('@', 1)[1]

        self.contact_email = _prompt('Contact email', validate=_check_email, required=False,
                                     help='The Contact page displays this email address to users. '
                                          'If empty, the Contact page will be hidden.')
        self.admin_email = _prompt('Admin email', default=self.contact_email, validate=_check_email,
                                   help='If an error occurs Indico sends an email to the admin address. '
                                        'This should be the address of whoever is the technical manager of this '
                                        'Indico instance, i.e. most likely you.')
        self.noreply_email = _prompt('No-reply email',
                                     default=_get_noreply_email(self.contact_email or self.admin_email),
                                     validate=_check_email,
                                     help='When Indico sends email notifications they are usually sent from the '
                                          'noreply email.')

    def _prompt_smtp_data(self, dev=False):
        def _get_default_smtp(custom_host=None):
            if custom_host:
                candidates = [(custom_host, 25), (custom_host, 587)]
            else:
                candidates = [('127.0.0.1', 25), ('127.0.0.1', 587)]
            if dev:
                candidates.insert(0, (custom_host or '127.0.0.1', 1025))
            for host, port in candidates:
                try:
                    SMTP(host, port, timeout=3).close()
                except Exception:  # noqa: S112
                    continue
                return host, port
            return '', ''

        first = True
        default_smtp_host, default_smtp_port = _get_default_smtp()
        smtp_user = ''
        while True:
            smtp_host = _prompt('SMTP host', default=default_smtp_host,
                                help=('Indico needs an SMTP server to send emails.' if first else None))
            if smtp_host != default_smtp_host:
                default_smtp_port = _get_default_smtp(smtp_host)[1]
            smtp_port = int(_prompt('SMTP port', default=str(default_smtp_port or 25)))
            smtp_user = _prompt('SMTP username', default=smtp_user, required=False,
                                help=('If your SMTP server requires authentication, '
                                      'enter the username now.' if first else None))
            smtp_password = _prompt('SMTP password', password=True) if smtp_user else ''
            first = False
            # Test whether we can connect
            try:
                smtp = SMTP(smtp_host, smtp_port, timeout=10)
                smtp.ehlo_or_helo_if_needed()
                if smtp_user and smtp_password:
                    smtp.starttls()
                    smtp.login(smtp_user, smtp_password)
                smtp.quit()
            except Exception as exc:
                _warn('SMTP connection failed: ' + str(exc))
                if not click.confirm('Keep these settings anyway?'):
                    default_smtp_host = smtp_host
                    default_smtp_port = smtp_port
                    continue
            self.smtp_host = smtp_host
            self.smtp_port = smtp_port
            self.smtp_user = smtp_user
            self.smtp_password = smtp_password
            break

    def _prompt_defaults(self):
        def _get_all_locales():
            # get all directories in indico/translations
            root = Path(get_root_path('indico')) / 'translations'
            return [ent.name for ent in root.iterdir() if ent.is_dir()]

        def _get_system_timezone():
            candidates = []
            # Get a timezone name directly
            etc_timezone = Path('/etc/timezone')
            if etc_timezone.is_file():
                candidates.append(etc_timezone.read_text().strip())
            # Get the timezone from the symlink
            etc_localtime = Path('/etc/localtime')
            if etc_localtime.is_symlink():
                candidates.append(re.sub(r'.*/usr/share/zoneinfo/', '', str(etc_localtime.readlink())))
            # We do not try to find a matching zoneinfo based on a checksum
            # as e.g. https://stackoverflow.com/a/12523283/298479 suggests
            # since this is ambiguous and we rather have the user type their
            # timezone if we don't have a very likely match.
            return next((str(tz) for tz in candidates if tz in common_timezones), '')

        self.default_locale = _prompt('Default locale', default='en_GB', list_=_get_all_locales(),
                                      help='Specify the default language/locale used by Indico.')

        self.default_timezone = _prompt('Default timezone', default=_get_system_timezone(), list_=common_timezones,
                                        validate=all_timezones.__contains__,
                                        help='Specify the default timezone used by Indico.')

    def _prompt_misc(self):
        self.rb_active = _confirm('Enable room booking?',
                                  help='Indico contains a room booking system. If you do not plan to maintain a list '
                                       'of rooms available in your organization in Indico and use it to book them, it '
                                       'is recommended to leave the room booking system disabled.')
        self.system_notices = _confirm('Enable system notices?', default=True,
                                       help='Indico can show important system notices (e.g. about security issues) to '
                                            'administrators. They are retrieved once a day without sending any data '
                                            'related to your Indico instance. It is strongly recommended to enable '
                                            'them.')
        self.public_user_search = _confirm('Enable public user search?', default=True,
                                            help='By default, authenticated users can search for other users by name, '
                                                 'email address and affiliation, and see this data for users in their '
                                                 'search results. This is usually expected in academic environments '
                                                 'where the convenience of being able to find someone to add them as '
                                                 'an abstract author is appreciated. In other environments, you may '
                                                 'prefer the increased privacy of not revealing email addresses of '
                                                 'users to other users, in which case you would need to disable this.\n'
                                                 'Please see the documentation for details if you are unsure:\n'
                                                 'https://docs.getindico.io/en/stable/config/settings/#ALLOW_PUBLIC_USER_SEARCH')

    def _setup(self, dev=False):
        storage_backends = {'default': 'fs:' + os.path.join(self.data_root_path, 'archive')}

        config_link_path = os.path.expanduser('~/.indico.conf')
        if not dev:
            create_config_link = _confirm('Create symlink?', default=True,
                                          help=f'By creating a symlink to indico.conf in {config_link_path}, you can '
                                               'run indico without having to set the INDICO_CONFIG environment '
                                               'variable')
        else:
            create_config_link = False

        config_data = [
            '# General settings',
            f'SQLALCHEMY_DATABASE_URI = {self.db_uri!r}',
            f'SECRET_KEY = {os.urandom(32)!r}',
            f'BASE_URL = {self.indico_url!r}',
            f'CELERY_BROKER = {self.redis_uri_celery!r}',
            f'REDIS_CACHE_URL = {self.redis_uri_cache!r}',
            f'DEFAULT_TIMEZONE = {self.default_timezone!r}',
            f'DEFAULT_LOCALE = {self.default_locale!r}',
            f'ENABLE_ROOMBOOKING = {self.rb_active!r}',
            'CACHE_DIR = {!r}'.format(os.path.join(self.data_root_path, 'cache')),
            'TEMP_DIR = {!r}'.format(os.path.join(self.data_root_path, 'tmp')),
            'LOG_DIR = {!r}'.format(os.path.join(self.data_root_path, 'log')),
            'LOCAL_PASSWORD_MIN_LENGTH = 15',  # use NIST recommendation for new installations
            f'STORAGE_BACKENDS = {storage_backends!r}',
            "ATTACHMENT_STORAGE = 'default'",
            '',
            '# Email settings',
            f'SMTP_SERVER = {(self.smtp_host, self.smtp_port)!r}',
            f'SMTP_USE_TLS = {bool(self.smtp_user and self.smtp_password)!r}',
            f'SMTP_LOGIN = {self.smtp_user!r}',
            f'SMTP_PASSWORD = {self.smtp_password!r}',
            f'SUPPORT_EMAIL = {self.admin_email!r}',
            f'PUBLIC_SUPPORT_EMAIL = {self.contact_email!r}',
            f'NO_REPLY_EMAIL = {self.noreply_email!r}'
        ]

        if dev:
            config_data += [
                '',
                '# Development options',
                'DB_LOG = True',
                'DEBUG = True',
                'SMTP_USE_CELERY = False'
            ]

        if not self.system_notices:
            config_data += [
                '',
                '# Disable system notices',
                'SYSTEM_NOTICES_URL = None'
            ]

        if not self.public_user_search:
            config_data += [
                '',
                '# Disable public user search',
                'ALLOW_PUBLIC_USER_SEARCH = False'
            ]

        config = '\n'.join(x for x in config_data if x is not None)

        if dev:
            if not os.path.exists(self.data_root_path):
                os.mkdir(self.data_root_path)

        _echo()
        for path in self._missing_dirs:
            _echo(cformat('%{magenta}Creating %{magenta!}{}%{reset}%{magenta}').format(path))
            os.mkdir(path)

        _echo(cformat('%{magenta}Creating %{magenta!}{}%{reset}%{magenta}').format(self.config_path))
        self.config_path.write_text(f'{config}\n')

        package_root = get_root_path('indico')
        _copy(os.path.normpath(os.path.join(package_root, 'logging.yaml.sample')),
              os.path.join(self.config_dir_path, 'logging.yaml'))

        if not dev:
            _link(os.path.join(package_root, 'web', 'static'), os.path.join(self.data_root_path, 'web', 'static'))
            _copy(os.path.join(package_root, 'web', 'indico.wsgi'),
                  os.path.join(self.data_root_path, 'web', 'indico.wsgi'),
                  force=True)

        if create_config_link:
            _link(self.config_path, config_link_path)

        _echo()
        _echo(cformat('%{green}Indico has been configured successfully!'))
        if not dev and not create_config_link:
            _echo(cformat('Run %{green!}export INDICO_CONFIG={}%{reset} to use your config file')
                  .format(self.config_path))

        _echo(cformat('You can now run %{green!}indico db prepare%{reset} to initialize your Indico database'))
