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

from rstgen.confparser import ConfigParser
from configparser import NoOptionError, NoSectionError
from .utils import GITLAB_CI
from .utils import JINJA_ENV
from .utils import which_certbot, resolve_web_server
from .utils import default_db_engine, resolve_db_engine
from .utils import Installer, ifroot, has_usergroup
from .utils import DB_ENGINES, BATCH_HELP
from .utils import DEFAULTSECTION
from synodal import REPOS_DICT, KNOWN_REPOS, FRONT_ENDS
from os.path import join
import os
import sys
import shutil
import secrets
import click
from pathlib import Path
from logging import getLogger
from urllib.parse import urlparse

logger = getLogger(__file__)


APPNAMES = [a.nickname for a in KNOWN_REPOS if a.settings_module]

UWSGI_SUPERVISOR_CONF = """\
# generated by getlino
[program:{prjname}-uwsgi]
command = /usr/bin/uwsgi --ini {project_dir}/nginx/uwsgi.ini --ignore-sigpipe
user = {usergroup}
umask = 0002
stopsignal = QUIT
"""

LINOD_SUPERVISOR_CONF = """\
# generated by getlino
[program:linod-{prjname}]
command={project_dir}/linod.sh
user = {usergroup}
umask = 0002
"""
LINOD_SH = """\
#!/bin/bash
set -e  # exit on error
PRJ={project_dir}
. $PRJ/{env_link}/bin/activate
exec python $PRJ/manage.py linod
"""

MAKE_SNAPSHOT_CRON_SH = """\
#!/bin/sh
# generated by getlino
sudo service supervisor stop
{project_dir}/make_snapshot.sh > /dev/null
sudo service supervisor start
"""

ONLY_ROOT = "Only root can do startsite with {}"


def default_shared_env():
    return DEFAULTSECTION.get('shared_env')


def default_site_domain():
    return DEFAULTSECTION.get('server_domain')

# 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.


STARTSITE_OPTIONS = []


def add(spec, default=None, help=None, type=None, required=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)
    if required is not None:
        kwargs.update(required=required)
    o = click.Option([spec], **kwargs)
    o.root_only = root_only
    o.default = DEFAULTSECTION.get(o.name, default)  # ~/.getlino.conf
    STARTSITE_OPTIONS.append(o)


add('--dev-repos',
    default='',
    help="List of packages for which to install development version")
add('--shared-env',
    default=default_shared_env,
    help="Directory with shared virtualenv")
add('--site-domain',  # site_domain
    default=default_site_domain,
    help="FQDN for this site (without 'http(s)://' prefix).")
add('--db-engine',
    help="Database engine to use.",
    type=click.Choice([e.name for e in DB_ENGINES]))
add('--db-port', help="Database port to use.")
add('--db-host',
    default='localhost',
    help="Database host name to use.")
add(
    '--db-user',
    help="Database user name to use. Leave empty to use the project name.")
add(
    '--db-password',
    help="Password for database user. Leave empty to generate a secure password.")


def startsite(
    ctx,
    appname,
    prjname,
    ini_file,
    batch,
    dev_repos,
    shared_env,
    db_engine,
    db_port,
    db_host,
    db_user,
    db_password,
    site_domain
):
    """
    Create a new Lino site.

    Two mandatory arguments must be given unless `--ini-file` is specified:

    APPNAME : The application to run on the new site. Say `getlino list` to see
    a list of choices.

    PRJNAME : The nickname for the new site. We recommend lower-case only and
    digits but no "-" or "_". Examples:  foo, foo2, mysite, first, The name
    must be unique for this Lino server and  will become a subdirectory of the
    `--sites-base` directory specified by `getlino configure`.


    """  # .format(appnames=' '.join(APPNAMES))
    if (not appname or not prjname) and not ini_file:
        click.echo(ctx.get_help(), color=ctx.color)
        ctx.exit()

    i = Installer(batch)

    if ini_file:
        assert appname is None and prjname is None
        main_cfg = ConfigParser()
        main_cfg.read_file(ini_file)
        try:
            appname = main_cfg.get('getlino', 'appname')
            app_package = prjname = main_cfg.get('getlino', 'prjname')
            git_repo = main_cfg.get('getlino', 'git_repo')
            repo_nickname = git_repo.split('.git')[0].split('/')[-1]
            settings_module = main_cfg.get('getlino', 'settings_module')
            # try:
            #     git_branch = main_cfg.get('getlino', 'git_branch')
            # except NoOptionError as e:
            #     git_branch = DEFAULTSECTION.get('branch')
            # try:
            #     frontend_branch = main_cfg.get('getlino', 'frontend_branch')
            # except NoOptionError as e:
            #     frontend_branch = DEFAULTSECTION.get('branch')
            try:
                front_end = main_cfg.get('getlino', 'front_end')
            except NoOptionError as e:
                front_end = None
            try:
                use_django_settings = main_cfg.get('getlino',
                                                   'use_django_settings')
            except NoOptionError as e:
                use_django_settings = False
        except (NoSectionError, NoOptionError) as e:
            with open(Path(__file__).parent / 'templates' / 'lino.ini',
                      'r') as f:
                click.echo((
                    "INI must contain a 'getlino' section (with optional 'front_end').\nSee DEFAULT ini format: \n\n"
                    + f.read()),
                           color=ctx.color)
            ctx.exit()

        if not front_end and not use_django_settings:
            front_end = REPOS_DICT.get(DEFAULTSECTION.get('front_end'), None)

    i.ask_options(STARTSITE_OPTIONS, locals())

    # update context and local vars after interactive prompt
    sites_base = DEFAULTSECTION.get('sites_base')
    local_prefix = DEFAULTSECTION.get('local_prefix')
    python_path_root = join(sites_base, local_prefix)
    project_dir = join(python_path_root, prjname)
    if not os.path.exists(project_dir):
        os.makedirs(project_dir, exist_ok=True)
    i.check_permissions(project_dir)
    if shared_env:
        envdir = shared_env
    else:
        envdir = join(project_dir, DEFAULTSECTION.get('env_link'))

    def get_dev_repos_dir():
        full_repos_dir = DEFAULTSECTION.get('repos_base')
        if not full_repos_dir:
            full_repos_dir = join(envdir, DEFAULTSECTION.get('repos_link'))
            if not os.path.exists(full_repos_dir):
                os.makedirs(full_repos_dir, exist_ok=True)
        i.check_permissions(full_repos_dir)
        return full_repos_dir

    def change_directory_into_dev_repos():
        os.chdir(get_dev_repos_dir())

    if ini_file:
        # def clone_pull_install_repo(repo_nn, repo_remote, branch, settings_module=None):
        def clone_pull_install_repo(repo_nn,
                                    repo_remote,
                                    settings_module=None):
            clonable = True
            if os.path.exists(join(project_dir, repo_nn)):
                os.chdir(join(project_dir, repo_nn))
                i.runcmd("git pull --rebase -q")
                clonable = False

            if clonable and os.path.exists(join(get_dev_repos_dir(), repo_nn)):
                os.chdir(join(get_dev_repos_dir(), repo_nn))
                i.runcmd("git pull --rebase -q")
                clonable = False

            if clonable:
                os.chdir(project_dir)
                # i.runcmd("git clone -q --depth 1 -b {} {} {}".format(branch, repo_remote, repo_nn))
                i.runcmd("git clone -q {} {}".format(repo_remote, repo_nn))
                for f in ['setup.py', 'pyproject.toml']:
                    if os.path.exists(join(project_dir, repo_nn, f)):
                        shutil.move(join(project_dir, repo_nn),
                                    join(get_dev_repos_dir(), repo_nn))
                        change_directory_into_dev_repos()
                        i.run_in_env(envdir,
                                     "pip install -q -e {}".format(repo_nn))
                        break

            if os.path.exists(join(project_dir, repo_nn)):
                init_file = join(project_dir, repo_nn, '__init__.py')
                if not os.path.exists(init_file):
                    with open(init_file, 'w') as f:
                        f.write("# Created by getlino\n")
                if settings_module is not None:
                    return "{}.{}.{}.{}".format(local_prefix, prjname,
                                                repo_nickname, settings_module)

        # sm = clone_pull_install_repo(repo_nickname, git_repo, git_branch, settings_module)
        sm = clone_pull_install_repo(repo_nickname, git_repo, settings_module)
        if sm is not None:
            settings_module = sm
        if front_end and isinstance(front_end, str):
            u = urlparse(front_end)
            if all((u.scheme, u.netloc)):
                # clone_pull_install_repo(front_end.split('.git')[0].split('/')[-1], front_end, frontend_branch)
                clone_pull_install_repo(
                    front_end.split('.git')[0].split('/')[-1], front_end)

    # if len(FOUND_CONFIG_FILES) == 0:
    #     raise click.UsageError(
    #         "This server is not yet configured. Did you run `sudo -H getlino configure`?")

    # if os.path.exists(prjpath):
    #     raise click.UsageError("Project directory {} already exists.".format(prjpath))

    web_server = resolve_web_server(DEFAULTSECTION.get('web_server'))
    if web_server and not ifroot():
        raise click.ClickException(ONLY_ROOT.format(web_server))

    https = DEFAULTSECTION.getboolean('https')
    linod = DEFAULTSECTION.getboolean('linod')
    journal = DEFAULTSECTION.getboolean('journal')
    if linod and web_server and ifroot() and web_server.name == 'nginx':
        asgi_server = True
    else:
        asgi_server = False

    # prod = DEFAULTSECTION.getboolean('prod')
    # contrib = DEFAULTSECTION.getboolean('contrib')
    # shared_env = DEFAULTSECTION.get('shared_env')
    admin_name = DEFAULTSECTION.get('admin_name')
    admin_email = DEFAULTSECTION.get('admin_email')
    server_domain = DEFAULTSECTION.get('server_domain')
    # if web_server:
    #     server_domain = prjname + "." + server_domain
    server_url = ("https://" if https else "http://") + site_domain
    secret_key = secrets.token_urlsafe(20)

    db_engine = resolve_db_engine(db_engine or DEFAULTSECTION.get('db_engine')
                                  or default_db_engine())

    if db_engine.needs_root and not ifroot():
        raise click.ClickException(ONLY_ROOT.format(db_engine))

    db_host = db_host or DEFAULTSECTION.get('db_host')
    db_port = db_port or DEFAULTSECTION.get(
        'db_port') or db_engine.default_port

    usergroup = DEFAULTSECTION.get('usergroup')

    if not ini_file:
        app = REPOS_DICT.get(appname, None)
        if app is None:
            raise click.ClickException(
                "Invalid application nickname '{}'".format(appname))
        settings_module = app.settings_module
        if not settings_module:
            raise click.ClickException(
                "{} is a library, not an application".format(appname))

        front_end = FRONT_ENDS.get(DEFAULTSECTION.get('front_end'), None)
        if front_end is None:
            raise click.ClickException(
                "Invalid front_end name '{}''".format(front_end))

        if dev_repos:
            for k in dev_repos.split():
                repo = REPOS_DICT.get(k, None)
                if repo is None or not repo.git_repo:
                    nicknames = ' '.join(
                        [r.nickname for r in KNOWN_REPOS if r.git_repo])
                    raise click.ClickException(
                        "Invalid repository name {}. "
                        "Allowed names are one or more of ({})".format(
                            k, nicknames))

        # if not i.check_overwrite(project_dir):
        #     raise click.Abort()

        # if not i.asroot and not shared_env:
        #     raise click.ClickException(
        #         "Cannot startsite in a development environment without a shared-env!")

        app_package = app.package_name
        # app_package = app.settings_module.split('.')[0]
        repo_nickname = app.git_repo.split('/')[-1]

    context = {}
    context.update(DEFAULTSECTION)
    pip_packages = set()
    if not ini_file:  # and not shared_env:
        if app.nickname not in dev_repos:
            pip_packages.add(app.package_name)
        if front_end.nickname not in dev_repos:
            pip_packages.add(front_end.package_name)

        # 20221223 pull updates internal dependencies automatically
        for nickname in app.extra_deps:
            if nickname not in dev_repos:
                pip_packages.add(REPOS_DICT[nickname].package_name)

    for pkgname in db_engine.python_packages.split():
        pip_packages.add(pkgname)

    if journal:
        pip_packages.add('systemd-python')

    if linod and asgi_server:
        pip_packages.add('daphne')
        i.apt_install('redis-server')

    context.update({
        "prjname": prjname,
        "appname": appname,
        "project_dir": project_dir,
        "repo_nickname": repo_nickname,
        "app_package": app_package,
        "app_settings_module": settings_module,
        "django_settings_module": "{}.{}.settings".format(local_prefix, prjname),
        "site_domain": site_domain,
        "server_domain": server_domain,
        "server_url": server_url,
        "dev_packages": ' '.join([
            a.nickname for a in KNOWN_REPOS if a.nickname in dev_repos]),
        "pip_packages": ' '.join(pip_packages),
        "db_name": os.environ.get('GITLAB_CI_DB_NAME', None) or prjname,
        "python_path": sites_base,
        "usergroup": usergroup,
        "provider": "lino" if not ini_file or (ini_file and not use_django_settings) else "django"
    })

    if os.path.exists(join(project_dir, 'settings.py')):
        what = "reinstall {appname} to {project_dir}"
        reinstall = True
    else:
        what = "create {appname} site in {project_dir}"
        reinstall = False
    what = what.format(**context)
    click.echo("Preparing to {}".format(what))

    upstream_port = None
    used_ports = set()
    cfgp = ConfigParser()
    if asgi_server:
        if reinstall:
            cfgp.read(join(project_dir, 'lino.ini'))
            if cfgp.has_section('getlino_asgi') and cfgp.has_option(
                    'getlino_asgi', 'upstream_port'):
                try:
                    upstream_port = cfgp.getint('getlino_asgi',
                                                'upstream_port')
                except ValueError as e:
                    if cfgp.get('getlino_asgi', 'upstream_port') != "":
                        raise e
            cfgp.clear()
        if upstream_port is None:
            for p in os.listdir(python_path_root):
                if p != prjname:
                    p = join(python_path_root, p)
                    if os.path.isdir(p):
                        lini = join(p, 'lino.ini')
                        if os.path.exists(lini):
                            cfgp.read(lini)
                            try:
                                up = cfgp.getint('getlino_asgi',
                                                 'upstream_port')
                                used_ports.add(up)
                            except (NoSectionError, NoOptionError) as e:
                                pass
                            except ValueError as e:
                                if cfgp.get('getlino_asgi',
                                            'upstream_port') != "":
                                    raise e
                            cfgp.clear()
            upstream_port = 8001
            while True:
                if upstream_port in used_ports:
                    upstream_port += 1
                else:
                    break

        context.update(upstream_port=upstream_port)

    if not batch:
        shared_env = click.prompt("Shared virtualenv", default=shared_env)
        # if asroot:
        #     server_url = click.prompt("Server URL ", default=server_url)
        #     admin_name = click.prompt("Administrator's full name", default=admin_name)
        #     admin_email = click.prompt("Administrator's full name", default=admin_email)

    create_db = False
    settings_file = join(project_dir, "settings.py")
    if not os.path.exists(settings_file):
        if DEFAULTSECTION.get('db_user') and not db_user:
            shared_user = True
            db_user = DEFAULTSECTION.get('db_user')
            db_password = DEFAULTSECTION.get('db_password')
        else:
            shared_user = False
            db_user = db_user or DEFAULTSECTION.get('db_user')
            db_password = db_password or DEFAULTSECTION.get('db_password') \
                or secrets.token_urlsafe(8)
            if not db_user:
                db_user = prjname
            create_db = db_engine.needs_user

        if create_db and not batch:
            click.echo(
                "User credentials (for {db_engine} on {db_host}:{db_port}):".
                format(**context))
            db_user = click.prompt("- user name", default=db_user)
            db_password = click.prompt("- user password", default=db_password)
            if not GITLAB_CI:
                db_port = click.prompt("- port", default=db_port)
            db_host = click.prompt("- host name", default=db_host)

        if not batch:
            secret_key = click.prompt("Site's secret key", default=secret_key)

        context.update({
            "db_host": db_host,
            "db_port": db_port,
            "db_user": db_user,
            "db_password": db_password,
            "secret_key": secret_key,
        })

    if not i.yes_or_no("Okay to {}?".format(what)):
        raise click.Abort()

    os.umask(0o002)

    os.makedirs(project_dir, exist_ok=True)
    i.jinja_write(join(project_dir, "manage.py"), **context)

    if not os.path.exists(settings_file):
        tplname = None
        if ini_file and use_django_settings:
            tplname = 'django_settings.py'
        i.jinja_write(settings_file, tplname, **context)

    # pull.sh script is now in the virtualenv's bin folder
    # i.jinja_write(join(project_dir, "pull.sh"), **context)
    # if ifroot():
    #     log_link = join(project_dir, 'log')
    #     if not os.path.exists(log_link):
    #         logdir = join(DEFAULTSECTION.get("log_base"), prjname)
    #         os.makedirs(logdir, exist_ok=True)
    #         with i.override_batch(True):
    #             i.check_permissions(logdir)
    #             os.symlink(logdir, log_link)
    #             i.write_logrotate_conf('lino-{}.conf'.format(prjname),
    #                                    join(logdir, "lino.log"))

    bb = DEFAULTSECTION.get("backups_base")
    if bb and ifroot():
        if i.jinja_write(join(project_dir, "make_snapshot.sh"), **context):
            i.make_file_executable(join(project_dir, "make_snapshot.sh"))
            backups_base_dir = join(bb, prjname)
            os.makedirs(backups_base_dir, exist_ok=True)
            with i.override_batch(True):
                i.check_permissions(backups_base_dir)

            fn = 'make_snapshot_{prjname}'.format(**context)
            i.write_cron_job(fn,
                             MAKE_SNAPSHOT_CRON_SH.format(**context),
                             freq="daily")

    if web_server:
        if asgi_server:
            i.jinja_write(join(project_dir, "asgi.py"), **context)
        else:
            i.jinja_write(join(project_dir, "wsgi.py"), **context)
            pth = join(project_dir, web_server.name)
            os.makedirs(pth, exist_ok=True)
            if web_server.name == "nginx":
                i.jinja_write(join(pth, "uwsgi.ini"), **context)
                i.jinja_write(join(pth, "uwsgi_params"), **context)
        if hasattr(web_server, 'python_packages'):
            for p in web_server.python_packages.split():
                pip_packages.add(p)

    os.makedirs(join(project_dir, 'media'), exist_ok=True)

    i.check_virtualenv(envdir, context)

    if shared_env:
        pth = join(project_dir, DEFAULTSECTION.get('env_link'))
        if not os.path.exists(pth):
            os.symlink(envdir, pth)
        static_root = join(shared_env, 'static_root')
        if not os.path.exists(static_root):
            os.makedirs(static_root, exist_ok=True)

    if dev_repos:
        change_directory_into_dev_repos()
        click.echo("dev_repos is {} --> {}".format(dev_repos,
                                                   dev_repos.split()))
        repos = []
        for nickname in dev_repos.split():
            lib = REPOS_DICT.get(nickname, None)
            if lib is None:
                raise click.ClickException(
                    "Invalid repository nickname {} in --dev-repos".format(
                        nickname))
            repos.append(lib)
        click.echo("Installing {} repositories...".format(len(repos)))
        for lib in repos:
            i.clone_repo(lib)
        for lib in repos:
            i.install_repo(lib, envdir)

    if len(pip_packages):
        click.echo("Installing {} Python packages...".format(
            len(pip_packages)))
        i.run_in_env(
            envdir,
            "pip install -q --upgrade {}".format(' '.join(pip_packages)))

    if web_server:
        filename = "{}.conf".format(prjname)
        conf_root = join("/etc/", web_server.service)
        avpth = join(conf_root, 'sites-available', filename)
        enpth = join(conf_root, 'sites-enabled', filename)

        if asgi_server:

            if not os.path.exists('/run/daphne'):
                from pwd import getpwnam
                os.makedirs('/run/daphne')
                os.chown('/run/daphne',
                         getpwnam('root').pw_uid,
                         getpwnam('www-data').pw_gid)

                # below code re-creates the /run/daphne directory on server reboot
                i.write_file('/usr/lib/tmpfiles.d/daphne.conf',
                             'd /run/daphne 0755 root www-data\n')

            conf_tpl = 'nginx_asgi.conf'

            site_ini = join(project_dir, 'lino.ini')
            if not os.path.exists(site_ini):
                txt = JINJA_ENV.get_template('lino.ini').render()
                i.write_file(site_ini, txt)
                i.check_permissions(site_ini)
            channels = f"linod_{prjname}"
            cfgp.read(site_ini)
            if not cfgp.has_section('getlino_asgi'):
                cfgp.add_section('getlino_asgi')
            if cfgp.has_option('getlino_asgi', 'channels'):
                channels = cfgp.get('getlino_asgi', 'channels')
            else:
                cfgp.set('getlino_asgi', 'channels', channels)
            cfgp.set('getlino_asgi', 'upstream_port', str(upstream_port))
            if ini_file:
                cfgp.read_file(ini_file)
            with open(site_ini, 'r+') as f:
                cfgp.update_file(f)
            cfgp.clear()

            context.update(num_procs=os.cpu_count(),
                           channels=channels,
                           envdir=envdir)

            tpl = JINJA_ENV.get_template('daphne_supervisor.conf')
            conf = tpl.render(**context)
            i.write_supervisor_conf(f"{prjname}-asgi.conf", conf)
        else:
            conf_tpl = web_server.name + ".conf"
            # shutil.copyfile(join(project_dir, 'nginx', filename), avpth)
            if web_server.name == "nginx":
                i.write_supervisor_conf(
                    '{}-uwsgi.conf'.format(prjname),
                    UWSGI_SUPERVISOR_CONF.format(**context))

        if i.jinja_write(avpth, conf_tpl, **context):
            with i.override_batch(True):
                if i.check_overwrite(enpth):
                    os.symlink(avpth, enpth)
        i.must_restart(web_server.service)

    if create_db and db_engine.needs_user and not GITLAB_CI:
        if not shared_user:
            db_engine.setup_user(i, context)
        db_engine.setup_database(i, prjname, db_user, db_host)

    os.chdir(project_dir)
    i.run_in_env(envdir, "python manage.py install --noinput")

    if linod and not asgi_server and ifroot():
        fn = join(project_dir, 'linod.sh')
        i.write_file(fn, LINOD_SH.format(**context))
        i.check_permissions(fn, executable=True, writeable=False)
        i.write_supervisor_conf('linod_{}.conf'.format(prjname),
                                LINOD_SUPERVISOR_CONF.format(**context))
        i.must_restart('supervisor')

    if not reinstall:
        # i.run_in_env(envdir, "python manage.py migrate --noinput")  # Not necessary, prep does it from initdb.
        i.run_in_env(envdir, "python manage.py prep --noinput")
    db_engine.after_prep(i, context)
    if ifroot():
        i.run_in_env(envdir, "python manage.py buildcache -b")
        i.run_in_env(envdir, "python manage.py makehelp")

    i.run_apt_install()
    i.restart_services()

    if web_server:
        # I imagine that we need to actually restart nginx
        # before running certbot-auto because otherwise certbot would add
        # its entries to the default because it does does not yet see the
        # new site.

        if https:
            certbot_cmd = which_certbot()
            if certbot_cmd is None:
                raise click.ClickException("Oops, certbot is not installed.")
            i.runcmd("{} --{} -d {}".format(certbot_cmd, web_server.name,
                                            server_domain))
            i.must_restart(web_server.service)

    if ini_file:
        ini_file.close()
    click.echo("The new site {} has been created.".format(prjname))


params = [
    click.Argument(['appname'],
                   metavar="APPNAME",
                   type=click.Choice(APPNAMES),
                   required=False),
    click.Argument(['prjname'], required=False),
    click.Option(['--ini-file'],  # ini_file
                 type=click.File('r+'), required=False,
                 help="Read options from an existing lino.ini file."),
    click.Option(['--batch/--no-batch'], default=False, help=BATCH_HELP),
] + STARTSITE_OPTIONS
startsite = click.pass_context(startsite)
startsite = click.Command('startsite',
                          callback=startsite,
                          params=params,
                          help=startsite.__doc__)
