"""mini-buildd's web application"""
import io
import logging
import os
import random

import django
import django.conf
import django.core.handlers.wsgi
import django.core.management

from mini_buildd import config, util

LOG = logging.getLogger(__name__)


def get(*args, **kwargs):
    return django.core.handlers.wsgi.WSGIHandler(*args, **kwargs)


class Call:
    """Run ``django-admin`` commands with error handling and smart logging"""

    def _log(self, log, msg):
        log("[django-admin %s] %s", self.name, msg)

    def _logf(self, log, f):
        f.seek(0)
        for line in f:
            self._log(log, line.strip())

    def __init__(self, name, *args, **kwargs):
        self.name = name
        self.error = None
        self.stdout, self.stderr = io.StringIO(), io.StringIO()

        try:
            django.core.management.call_command(self.name, *args, **kwargs, stdout=self.stdout, stderr=self.stderr)
        except BaseException as e:
            self.error = e
            raise
        finally:
            self._logf(LOG.info, self.stdout)
            self._logf(LOG.warning, self.stderr)
            if self.error is not None:
                self._log(LOG.error, str(self.error))


def migrate():
    try:
        Call("migrate", interactive=False, run_syncdb=True)
    except django.db.OperationalError:
        LOG.info("Retrying migration with '--fake-initial'...")
        Call("migrate", interactive=False, run_syncdb=True, fake_initial=True)


def init():
    migrate()
    Call("check")
    config.SqlitePath().fix_perms()

    # For temporary debugging/check settings only
    # Call("diffsettings", output="unified")


def set_admin_password(password):
    """
    Set the password for the administrator

    :param password: The password to use.
    :type password: string
    """
    # This import needs the django app to be already configured (since django 1.5.2)
    from django.contrib.auth import models  # pylint: disable=import-outside-toplevel

    try:
        user = models.User.objects.get(username="admin")
        LOG.info("Updating 'admin' user's password...")
        user.set_password(password)
        user.save()
    except models.User.DoesNotExist:
        LOG.info("Creating 'admin' user with password...")
        models.User.objects.create_superuser("admin", "root@localhost", password)


def remove_system_artifacts():
    """Bulk-remove all model instances that might have produced cruft on the system (outside mini-buildd's home)"""
    for chroot in util.models().Chroot.mbd_get_prepared():
        try:
            chroot.Admin.mbd_remove(chroot)
        except BaseException as e:
            LOG.warning("Failed to remove %s: %s", chroot, e)


def loaddata(file_name):
    Call("loaddata", file_name)


def dumpdata(app_path):
    LOG.debug("Dumping data for: %s", app_path)
    Call("dumpdata", app_path, indent=2, format="json")


# Formerly in django_settings.py
MBD_INSTALLED_APPS = (
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.messages",
    "django.contrib.admin",
    "django.contrib.sessions",
    "django.contrib.admindocs",
    "django.contrib.staticfiles",
    "mini_buildd",
)


class SMTPCreds():
    """
    SMTP creds string parser -- format "USER:PASSWORD@smtp|ssmtp://HOST:PORT"

    >>> d = SMTPCreds(":@smtp://localhost:25")
    >>> (d.user, d.password, d.protocol, d.host, d.port)
    ('', '', 'smtp', 'localhost', 25)
    >>> d = SMTPCreds("kuh:sa:ck@smtp://colahost:44")
    >>> (d.user, d.password, d.protocol, d.host, d.port)
    ('kuh', 'sa:ck', 'smtp', 'colahost', 44)
    """

    def __init__(self, creds):
        self.creds = creds
        at = creds.partition("@")

        usrpass = at[0].partition(":")
        self.user = usrpass[0]
        self.password = usrpass[2]

        smtp = at[2].partition(":")
        self.protocol = smtp[0]

        hopo = smtp[2].partition(":")
        self.host = hopo[0][2:]
        self.port = int(hopo[2])


def gen_django_secret_key():
    # use same randomize-algorithm as in "django/core/management/commands/startproject.py"
    return "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for _i in range(50)])


def get_django_secret_key(home):
    """
    Create django's SECRET_KEY *once* and/or returns it

    :param home: mini-buildd's home directory.
    :type home: string
    :returns: string -- the (created) key.
    """
    secret_key_filename = os.path.join(home, ".django_secret_key")

    # the key to create or read from file
    secret_key = ""

    if not os.path.exists(secret_key_filename):
        # use same randomize-algorithm as in "django/core/management/commands/startproject.py"
        secret_key = gen_django_secret_key()
        with os.fdopen(os.open(secret_key_filename, os.O_CREAT | os.O_WRONLY, 0o600), "w") as secret_key_fd:
            secret_key_fd.write(secret_key)
    else:
        with util.fopen(secret_key_filename, "r") as existing_file:
            secret_key = existing_file.read()

    return secret_key


def configure(smtp_string):
    """Configure django"""
    LOG.debug("Setting up django...")

    smtp = SMTPCreds(smtp_string)
    debug = "webapp" in config.DEBUG

    django.conf.settings.configure(
        DEBUG=debug,

        DEFAULT_AUTO_FIELD="django.db.models.AutoField",  # django 3.2: https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys

        ALLOWED_HOSTS=["*"],

        EMAIL_HOST=smtp.host,
        EMAIL_PORT=smtp.port,
        EMAIL_USE_TLS=smtp.protocol == "ssmtp",
        EMAIL_HOST_USER=smtp.user,
        EMAIL_HOST_PASSWORD=smtp.password,

        AUTH_PASSWORD_VALIDATORS=[
            {
                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
            },
            {
                "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
                "OPTIONS": {
                    "min_length": 8,
                }
            },
            {
                "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
            },
            {
                "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
            },
        ],

        DATABASES={
            "default": {
                "ENGINE": "django.db.backends.sqlite3",
                "NAME": config.SqlitePath().path,
            }},

        TIME_ZONE="Etc/UTC",  # Default time zone: Canonical UTC. Better than django's default 'America/Chicago'.
        USE_TZ=True,          # Django: "stores datetime information in UTC in the database": that's definitely what we want.
        USE_L10N=True,

        SECRET_KEY=get_django_secret_key(config.ROUTES["home"].path.join()),
        ROOT_URLCONF="mini_buildd.urls",
        STATIC_URL=config.URIS["static"]["static"].uri,
        ACCOUNT_ACTIVATION_DAYS=3,
        LOGIN_URL=config.URIS["accounts"]["login"].uri,

        MIDDLEWARE=[
            "django.middleware.common.CommonMiddleware",
            "django.contrib.sessions.middleware.SessionMiddleware",
            "django.middleware.csrf.CsrfViewMiddleware",
            "django.contrib.auth.middleware.AuthenticationMiddleware",
            "django.contrib.messages.middleware.MessageMiddleware",
            "mini_buildd.views.ExceptionMiddleware"
        ],

        INSTALLED_APPS=MBD_INSTALLED_APPS,
        TEMPLATES=[
            {
                "BACKEND": "django.template.backends.django.DjangoTemplates",
                "DIRS": [
                    f"{config.PY_PACKAGE_PATH}/mini_buildd/templates"
                ],
                "APP_DIRS": True,
                "OPTIONS": {
                    "debug": debug,
                    "context_processors": [
                        #: Django defaults as per ``django-admin startproject dummy`` (django 2.2.20)
                        "django.template.context_processors.debug",
                        "django.template.context_processors.request",
                        "django.contrib.auth.context_processors.auth",
                        "django.contrib.messages.context_processors.messages",
                        #: Custom
                        "mini_buildd.views.context",
                    ],
                    "builtins": [
                        "mini_buildd.builtins",
                        "django.templatetags.static",
                    ],
                },
            },
        ])

    django.setup()


def pseudo_configure(sqlite_path=":memory:"):
    """
    Pseudo-configure django (minimal setup to access models and run admin)

    Used internally only (where you need mini-buildd's model classes, but no actual instances): sphinx doc, run doctest, django SQL migrations, ...
    """
    django.conf.settings.configure(
        SECRET_KEY=gen_django_secret_key(),
        DATABASES={
            "default": {
                "ENGINE": "django.db.backends.sqlite3",
                "NAME": sqlite_path,
            }
        },
        MIDDLEWARE=[
            "django.contrib.sessions.middleware.SessionMiddleware",
            "django.contrib.auth.middleware.AuthenticationMiddleware",
            "django.contrib.messages.middleware.MessageMiddleware",
        ],
        TEMPLATES=[
            {
                "BACKEND": "django.template.backends.django.DjangoTemplates",
                "OPTIONS": {
                    "context_processors": [
                        "django.contrib.auth.context_processors.auth",
                        "django.contrib.messages.context_processors.messages",
                    ],
                },
            },
        ],
        INSTALLED_APPS=MBD_INSTALLED_APPS)

    django.setup()
