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

import os, sys
import shutil
import secrets
import click
from pathlib import Path
from logging import getLogger
from urllib.parse import urlparse

logger = getLogger(__file__)

from os.path import join

from synodal import REPOS_DICT, KNOWN_REPOS, FRONT_ENDS
from .utils import DEFAULTSECTION
from .utils import DB_ENGINES, BATCH_HELP
from .utils import Installer, ifroot, has_usergroup
from .utils import default_db_engine, resolve_db_engine
from .utils import which_certbot, resolve_web_server
from .utils import JINJA_ENV
from .utils import GITLAB_CI

from configparser import NoOptionError, NoSectionError
from rstgen.confparser import ConfigParser

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')


@click.command()
@click.argument('appname',
                metavar="APPNAME",
                type=click.Choice(APPNAMES),
                required=False)
@click.argument('prjname', required=False)
@click.option('--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)
@click.option('--dev-repos',
              default='',
              help="List of packages for which to install development version")
@click.option('--shared-env',
              default=default_shared_env,
              help="Directory with shared virtualenv")
@click.option('--db-engine',
              help="Database engine to use.",
              type=click.Choice([e.name for e in DB_ENGINES]))
@click.option('--db-port', help="Database port to use.")
@click.option('--db-host',
              default='localhost',
              help="Database host name to use.")
@click.option(
    '--db-user',
    help="Database user name to use. Leave empty to use the project name.")
@click.option(
    '--db-password',
    help=
    "Password for database user. Leave empty to generate a secure password.")
@click.pass_context
def startsite(
    ctx,
    appname,
    prjname,
    ini_file,
    batch,
    dev_repos,
    shared_env,
    db_engine,
    db_port,
    db_host,
    db_user,
    db_password,
):
    """
    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)

    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)
    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))

    linod = DEFAULTSECTION.getboolean('linod')
    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 DEFAULTSECTION.getboolean('https') else "http://") \
                 + server_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 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),
        "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 collectstatic --noinput")
        i.run_in_env(envdir, "python manage.py makehelp")
        i.run_in_env(envdir, "python manage.py buildcache")

    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 DEFAULTSECTION.getboolean('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))
