# Copyright 2019-2025 Rumma & Ko Ltd
# License: GNU Affero General Public License v3 (see file COPYING for details)

import os
from os.path import join
import distro
import click
from pathlib import Path

from synodal import KNOWN_REPOS, FRONT_ENDS

from .utils import CONFIG, CONF_FILES, DEFAULTSECTION
from .utils import DB_ENGINES, BATCH_HELP
from .utils import Installer, ifroot, default_db_engine, resolve_db_engine
from .utils import which_certbot
from .utils import WEB_SERVERS, resolve_web_server, GITLAB_CI

HEALTHCHECK_NAME = '/usr/local/bin/healthcheck.sh'
RELOAD_NAME = '/usr/local/bin/reload_services.sh'

CERTBOT_AUTO_RENEW = """\
# generated by getlino
0 0,12 * * * root python -c 'import random; import time; time.sleep(random.random() * 3600)' && {} renew
"""

MONIT_CONF = """\
# generated by getlino
set alert root@localhost with reminder on 2 cycles
check program status with path {}
    if status != 0 then alert
check device ROOT  with path /
    if SPACE usage > 95% then alert
check system $HOST
    if memory usage > 75% for 5 cycles then alert
""".format(HEALTHCHECK_NAME)

# HEALTHCHECK_CRON_JOB = """\
# #!/bin/bash
# /usr/local/bin/healthcheck.sh > /dev/null
# # we redirect STDOUT to null, but not STDERR because if there is an error we want cron to notify the admins.
# """

LIBREOFFICE_SUPERVISOR_CONF = """\
# generated by getlino
[program:libreoffice]
command = libreoffice --accept="socket,host=127.0.0.1,port=8100;urp;" --nologo --headless --nofirststartwizard
umask = 0002
"""

SHARED_SETTINGS = r"""\
# generated by getlino
# this is the shared settings file, imported by every Lino site on this server
ADMINS = [
  ["{admin_name}", "{admin_email}"]
]
EMAIL_HOST = 'localhost'
SERVER_EMAIL = 'noreply@{server_domain}'
DEFAULT_FROM_EMAIL = 'noreply@{server_domain}'
STATIC_ROOT = 'env/static_root'
TIME_ZONE = "{time_zone}"
# USE_TZ = True
# TIME_ZONE = 'UTC'
# TIME_ZONE = 'Europe/Brussels'
# TIME_ZONE = 'Europe/Tallinn'
# List of accepted time zones:
# import pytz; print("\n".join(pytz.all_timezones))
"""

BASH_ALIASES = """\
#!/bin/bash
# generated by getlino
alias a='. env/bin/activate'
alias ll='ls -alF'
alias pm='python manage.py'
alias runserver='LINO_LOGLEVEL=DEBUG python manage.py runserver'
function pywhich() {{
  python -c "import $1; print($1.__file__)"
}}
"""
BASH_ALIASES_GO = """
function go() {{
    for BASE in {go_bases}
    do
      if [ -d $BASE/$1 ] ; then
        cd $BASE/$1;
        return;
      fi
    done
    echo Oops: no project $1
    return -1
}}
"""

BASH_ALIASES_DEV = """
alias pp='per_project'
alias ci='inv ci'
alias dt='doctest'
"""

# The configure command will be decorated below. We cannot use decorators
# because we define the list of options in CONFIGURE_OPTIONS because we need
# that list also for asking questions using the help text.

CONFIGURE_OPTIONS = []


def add(spec, default, help, type=None, root_only=False):
    """
    :param root_only: if user is not root, do not ask for the user for the choice.

    """
    kwargs = dict()
    kwargs.update(help=help)
    if type is not None:
        kwargs.update(type=type)
    o = click.Option([spec], **kwargs)
    o.root_only = root_only
    o.default = DEFAULTSECTION.get(o.name, default)  # ~/.getlino.conf
    CONFIGURE_OPTIONS.append(o)


def default_sites_base():
    return ifroot('/usr/local/lino', os.path.expanduser('~/lino'))


def default_shared_env():
    return ifroot('', os.environ.get('VIRTUAL_ENV', ''))
    # return os.environ.get('VIRTUAL_ENV', '/usr/local/lino/venvs')


# def default_repos_base():
#     #if default_shared_env():
#     return ifroot('/usr/local/lino/repositories', os.path.expanduser('~/lino/repositories'))
#     #return ''

# must be same order as in signature of configure command below
# add('--prod/--no-prod', True, "Whether this is a production server")
add('--sites-base', default_sites_base,
    'Base directory for Lino sites on this server')
add('--local-prefix', 'lino_local',
    "Prefix for local server-wide importable packages")
add('--shared-env', default_shared_env,
    "Root directory of your shared virtualenv")
add('--repos-base', '', "Base directory for shared code repositories")
# add('--repos-base', default_repos_base, "Base directory for shared code repositories")
add('--clone/--no-clone', False,
    "Clone all contributor repositories and install them to the shared-env")
# add('--branch', 'master', "The git branch to use for --clone")
add('--webdav/--no-webdav',
    True,
    "Whether to enable webdav on new sites",
    root_only=True)
add('--backups-base',
    '/var/backups/lino',
    'Base directory for backups',
    root_only=True)
# add('--log-base',
#     '/var/log/lino',
#     'Base directory for log files',
#     root_only=True)
add('--usergroup',
    'www-data',
    "User group for files to be shared with the web server",
    root_only=True)
add('--supervisor-dir',
    '/etc/supervisor/conf.d',
    "Directory for supervisor config files",
    root_only=True)
add('--env-link', 'env', "link to virtualenv (relative to project dir)")
add('--repos-link', 'repositories',
    "link to code repositories (relative to virtualenv)")
add('--appy/--no-appy',
    ifroot,
    "Whether this server provides appypod and LibreOffice",
    root_only=True)
add('--journal/--no-journal',
    ifroot,
    "Whether Lino sites should log to systemd journal",
    root_only=True)
add('--weasyprint/--no-weasyprint',
    ifroot,
    "Whether this server provides weasyprint",
    root_only=True)
add('--redis/--no-redis', ifroot, "Whether this server provides redis")
add('--devtools/--no-devtools', lambda: not ifroot(),
    "Whether to install development tools (build docs and run tests)")
add('--server-domain', 'localhost', "Domain name of this server")
add('--https/--no-https',
    False,
    "Whether this server uses secure http",
    root_only=True)
add('--ldap/--no-ldap',
    False,
    "Whether this server works as an LDAP server",
    root_only=True)
add('--monit/--no-monit',
    True,
    "Whether this server uses monit",
    root_only=True)
add('--web-server',
    '',
    "Which web server to use here.",
    click.Choice([e.name for e in WEB_SERVERS] + ['']),
    root_only=True)
add('--db-engine', default_db_engine, "Default database engine for new sites.",
    click.Choice([e.name for e in DB_ENGINES]))
add('--db-port', '', "Default database port to use for new sites.")
add('--db-host', 'localhost', "Default database host name for new sites.")
add(
    '--db-user', '',
    "Default database user name for new sites. Leave empty to use the project name."
)
add(
    '--db-password', '',
    "Default database password for new sites. Leave empty to generate a secure password."
)
add('--admin-name', 'Joe Dow', "The full name of the server administrator")
add('--admin-email', 'joe@example.com',
    "The email address of the server administrator")
add('--time-zone', 'Europe/Brussels', "The TIME_ZONE to set on new sites")
add('--linod/--no-linod', True, "Whether new sites use linod", root_only=True)
add('--languages', 'en', "The languages to set on new sites")
add('--front-end', 'lino_react.react', "The front end to use on new sites",
    click.Choice(FRONT_ENDS.keys()))


def configure(ctx, batch, sites_base, local_prefix, shared_env, repos_base,
              # clone, webdav, backups_base, log_base, usergroup, supervisor_dir,
              clone, webdav, backups_base, usergroup, supervisor_dir,
              env_link, repos_link, appy, journal, weasyprint, redis, devtools, server_domain,
              https, ldap, monit, db_engine, db_port, db_host, db_user,
              db_password, admin_name, admin_email, time_zone, linod,
              languages, front_end, web_server):
    """
    Configure this machine to become a Lino production server.
    """

    # if len(FOUND_CONFIG_FILES) > 1:
    #     # reconfigure is not yet supported
    #     raise click.UsageError("Found multiple config files: {}".format(
    #         FOUND_CONFIG_FILES))

    i = Installer(batch)
    context = {}
    context.update(locals())
    context.update({
        "prjname": '',
        "appname": '',
        "project_dir": '',
        "repo_nickname": '',
        "app_package": '',
        "app_settings_module": '',
        "django_settings_module": '',
        "dev_packages": ' '.join([a.nickname for a in KNOWN_REPOS if a.git_repo]),
        "pip_packages": '',
        "python_path": ''
    })

    conffile = ifroot(CONF_FILES[0], CONF_FILES[1])
    click.echo("This will write to configuration file {}".format(conffile))

    # before asking questions check whether we will be able to store them
    pth = os.path.dirname(conffile)
    if not os.path.exists(pth):
        os.makedirs(pth, exist_ok=True)
    if not os.access(pth, os.W_OK):
        raise click.ClickException(
            "No write permission for directory {}".format(pth))
    if os.path.exists(conffile) and not os.access(conffile, os.W_OK):
        raise click.ClickException(
            "No write permission for file {}".format(conffile))

    if clone and not shared_env:
        shared_env = os.environ.get('VIRTUAL_ENV', '')

    # if shared_env: not sure whether this is a good idea
    #     shared_env = os.path.abspath(shared_env)

    i.ask_options(CONFIGURE_OPTIONS, locals())

    # update context and local vars after interactive prompt
    context.update(DEFAULTSECTION)
    clone = DEFAULTSECTION.getboolean('clone')
    shared_env = DEFAULTSECTION.get('shared_env')
    db_user = DEFAULTSECTION.get('db_user')
    db_password = DEFAULTSECTION.get('db_password')
    db_engine = resolve_db_engine(DEFAULTSECTION.get('db_engine'))
    web_server = resolve_web_server(DEFAULTSECTION.get('web_server'))
    appy = DEFAULTSECTION.getboolean('appy')
    weasyprint = DEFAULTSECTION.getboolean('weasyprint')
    supervisor = web_server or appy
    devtools = DEFAULTSECTION.getboolean('devtools')

    # combined validations
    if clone and not shared_env:
        raise click.ClickException("Cannot use --clone without --shared-env")
    if db_user and not db_password:
        raise click.Error(
            "If you set a shared --db-user you must also set a shared --db-password"
        )

    if not i.yes_or_no("Write above options to {} ?".format(conffile)):
        raise click.Abort()

    with open(conffile, 'w') as fd:
        CONFIG.write(fd)
    click.echo("Wrote config file " + conffile)

    if not i.yes_or_no("Start installing Lino according to above settings?"):
        raise click.Abort()

    i.apt_install("git python3 python3-dev python3-setuptools python3-pip")
    i.apt_install("libffi-dev libssl-dev")  # maybe needed for weasyprint
    i.apt_install("build-essential")  # needed for installing mysqlclient
    i.apt_install("swig")  # required to install eidreader
    i.apt_install(
        "libldap2-dev libsasl2-dev")  # Needed e.g. by django_auth_ldap
    # i.apt_install("python2.7-dev libldap2-dev libsasl2-dev ldap-utils lcov") # Needed by Ldap package
    # i.runcmd("sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install slapd")

    if supervisor:
        i.apt_install("supervisor")

    if web_server and ifroot():
        i.apt_install("cron")
        i.apt_install(web_server.apt_packages)
        i.apt_install("logrotate")
        i.must_restart(web_server.service)

    if devtools:
        i.apt_install("subversion graphviz sqlite3")

    if DEFAULTSECTION.getboolean('monit'):
        i.apt_install("monit")

    if DEFAULTSECTION.getboolean('redis') and not GITLAB_CI:
        i.apt_install("redis-server")
        i.must_restart('redis-server')

    if not GITLAB_CI:
        i.apt_install(db_engine.apt_packages)

    if web_server:
        i.apt_install(web_server.apt_packages)

    if db_engine.service and not GITLAB_CI:
        i.must_restart(db_engine.service)

    if appy:
        i.apt_install("libreoffice-common python3-uno")
        i.apt_install("tidy")

    if journal:
        i.apt_install("libsystemd-dev")

    if weasyprint:
        # https://doc.courtbouillon.org/weasyprint/stable/first_steps.html
        i.apt_install(
            "python3-pip libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0")

    if DEFAULTSECTION.getboolean('ldap'):
        i.apt_install("slapd ldap-utils")

    if ifroot():
        # for k in ("log_base", "backups_base"):
        for k in ("backups_base",):
            pth = DEFAULTSECTION.get(k)
            if not pth:
                print("Strange: {} is empty...".format(k))
                continue
            if not os.path.exists(pth):
                if batch or i.yes_or_no("Create {} {} ?".format(k, pth),
                                        default=True):
                    os.makedirs(pth, exist_ok=True)
            i.check_permissions(pth)
        i.apt_install("zip")

    if ifroot() and DEFAULTSECTION.getboolean('monit'):
        # click.echo("20200727 Install monit...")
        # Debian buster didn't include monit for administrative reasons,
        # so we must add a backport.
        if distro.id() == "debian" and distro.codename() == "buster":
            if batch or i.yes_or_no("Add backport for monit on Debian buster?",
                                    default=True):
                click.echo("Adding backport for monit on Debian buster...")
                with i.override_batch(True):
                    pth = Path("/etc/apt/sources.list.d/buster-backports.list")
                    myline = "deb http://ftp.de.debian.org/debian buster-backports main"
                    if pth.is_file():
                        content = pth.open().read()
                        if not myline in content:
                            content += "\n" + myline + "\n"
                    else:
                        content = myline + "\n"
                    i.write_file(pth, content)
                    # i.runcmd('printf "%s\\n" "deb http://ftp.de.debian.org/debian buster-backports main" | sudo tee /etc/apt/sources.list.d/buster-backports.list')
                    # i.runcmd('echo "deb http://ftp.de.debian.org/debian buster-backports main" >> /etc/apt/sources.list.d/buster-backports.list')

    i.run_apt_install()
    i.restart_services()

    if DEFAULTSECTION.get('db_user') and not GITLAB_CI:
        db_engine.setup_user(i, context)

    go_bases = []

    if clone:
        click.echo("Installing repositories for shared-env...")
        if not shared_env:
            raise click.ClickException(
                "With --clone you must either "
                "have a virtualenv activated or specify one with --shared-env")

        repos_base = DEFAULTSECTION.get('repos_base')
        if not repos_base:
            repos_base = join(shared_env, DEFAULTSECTION.get('repos_link'))

        if not os.path.exists(repos_base):
            if batch or i.yes_or_no(
                    "Create base directory for repositories {} ?".format(
                        repos_base),
                    default=True):
                os.makedirs(repos_base, exist_ok=True)
        i.check_permissions(repos_base)

        i.check_virtualenv(shared_env, context)
        os.chdir(repos_base)
        repos = [r for r in KNOWN_REPOS if r.git_repo]
        if batch or i.yes_or_no(
                "Clone repositories to {} ?".format(repos_base), default=True):
            with i.override_batch(True):
                for repo in repos:
                    i.clone_repo(repo)
        if batch or i.yes_or_no(
                "Install cloned repositories to {} ?".format(shared_env),
                default=True):
            # getlino must not reinstall itself because that causes at least
            # TemplateNotFound issues
            with i.override_batch(True):
                for repo in repos:
                    if repo.package_name:
                        if repo.package_name != "getlino":
                            i.install_repo(repo, shared_env)
        go_bases.append(repos_base)
        for repo in repos:
            if repo.package_name:
                name = repo.package_name.replace("-", "_")
                pth = Path(repos_base, repo.nickname, name, "projects")
                if pth.exists():
                    go_bases.append(str(pth))
                # else:
                #     print("20240708 no projects dir in", repo)

    pth = DEFAULTSECTION.get('sites_base')
    if not os.path.exists(pth):
        if batch or i.yes_or_no(
                "Create base directory for sites {} ?".format(pth),
                default=True):
            os.makedirs(pth, exist_ok=True)
    i.check_permissions(pth)

    local_prefix = DEFAULTSECTION.get('local_prefix')
    pth = join(DEFAULTSECTION.get('sites_base'), local_prefix)
    if os.path.exists(pth):
        i.check_permissions(pth)
    elif batch or i.yes_or_no(
            "Create shared settings package {} ?".format(pth), default=True):
        os.makedirs(pth, exist_ok=True)
        i.check_permissions(pth)
    with i.override_batch(True):
        i.check_permissions(pth)
        fn = join(pth, '__init__.py')
        i.write_file(fn, '')
        i.check_permissions(fn)
    fn = join(pth, 'settings.py')
    i.write_file(fn, SHARED_SETTINGS.format(**DEFAULTSECTION))
    i.check_permissions(fn)
    go_bases.append(pth)

    pth = ifroot('/etc/getlino/lino_bash_aliases',
                 os.path.expanduser('~/.lino_bash_aliases'))
    ctx = dict(DEFAULTSECTION)
    content = BASH_ALIASES.format(**ctx)
    if len(go_bases):
        ctx.update(go_bases=" ".join(go_bases))
        content += BASH_ALIASES_GO.format(**ctx)
    if devtools:
        content += BASH_ALIASES_DEV
    i.write_file(pth, content)
    i.check_permissions(pth, writeable=False)
    click.echo(
        "Note: please add manually the following line to your .bashrc file:\nsource {}"
        .format(pth))

    if ifroot():
        if supervisor:
            i.write_logrotate_conf('supervisor.conf',
                                   '/var/log/supervisor/supervisord.log')
            i.must_restart('supervisor')

        i.jinja_write(RELOAD_NAME, **context)
        i.check_permissions(RELOAD_NAME, executable=True)

        if DEFAULTSECTION.getboolean('monit'):
            i.jinja_write(HEALTHCHECK_NAME, **context)
            i.check_permissions(HEALTHCHECK_NAME, executable=True)
            i.write_file('/etc/monit/conf.d/lino.conf', MONIT_CONF)
            # seems that monit creates its own logrotate config file
            # i.write_logrotate_conf(
            #     'monit.conf', '/var/log/monit.log')

        # i.write_cron_job('healthcheck',
        #                  HEALTHCHECK_CRON_JOB.format(**context),
        #                  freq="hourly")

        if supervisor and appy:
            i.write_supervisor_conf(
                'libreoffice.conf',
                LIBREOFFICE_SUPERVISOR_CONF.format(**DEFAULTSECTION))
            i.must_restart('supervisor')

        # if DEFAULTSECTION.get('db_engine') == 'mysql':
        #     i.runcmd("mysql_secure_installation")
        # not tested because it interactively asks for root password

        if DEFAULTSECTION.getboolean('https'):
            certbot_cmd = which_certbot()
            if certbot_cmd:
                click.echo("{} already installed".format(certbot_cmd))
            if certbot_cmd is None:
                if batch or i.yes_or_no("Install certbot?", default=True):
                    # i.apt_install("certbot python-certbot-nginx")
                    i.apt_install("snapd")
                    i.run_apt_install()
                    i.runcmd_sudo("snap install core")
                    i.runcmd_sudo("snap refresh core")
                    i.runcmd_sudo("snap install --classic certbot")
                    i.runcmd_sudo("ln -s /snap/bin/certbot /usr/bin/certbot")
                    certbot_cmd = "certbot"
            # if certbot_cmd is None:
            #     if batch or i.yes_or_no("Install certbot-auto?", default=True):
            #         with i.override_batch(True):
            #             i.runcmd("wget https://dl.eff.org/certbot-auto")
            #             i.runcmd("mv certbot-auto /usr/local/bin/certbot-auto")
            #             i.runcmd("chown root /usr/local/bin/certbot-auto")
            #             i.runcmd("chmod 0755 /usr/local/bin/certbot-auto")
            #             i.runcmd("certbot-auto -n")
            #             i.runcmd("certbot-auto register --agree-tos -m {} -n".format(DEFAULTSECTION.get('admin_email')))
            #         certbot_cmd = "/usr/local/bin/certbot-auto"
            if certbot_cmd:
                if batch or i.yes_or_no(
                        "Set up automatic certificate renewal?", default=True):
                    i.write_file('/etc/cron.d/getlino-certbot.conf',
                                 CERTBOT_AUTO_RENEW.format(certbot_cmd))

        if DEFAULTSECTION.getboolean('ldap'):
            i.runcmd("dpkg-reconfigure slapd")

    i.restart_services()

    click.echo("getlino configure completed.")


params = [
    click.Option(['--batch/--no-batch'], default=False, help=BATCH_HELP),
] + CONFIGURE_OPTIONS
configure = click.pass_context(configure)
configure = click.Command('configure',
                          callback=configure,
                          params=params,
                          help=configure.__doc__)
