# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""
Default Django settings for the Debusine project.

Most settings are documented in this file and they are initialized to some
reasonable default values when possible.  They will be extended (and
possibly overridden) by settings from the other modules in this package
depending on the setup selected by the administrator. You likely won't
have to modify that file.

You should instead create local.py to put your site-specific settings (use
local.py.sample as template).

Here are the most important settings:

:py:data:`DEBUSINE_FQDN`
    The fully qualified domain name of the debusine installation.
    It should be a service-specific DNS entry like "debusine.example.com".
    Defaults to the FQDN of the machine which might not be adequate.

:py:data:`DEBUSINE_DATA_PATH`
    The directory where debusine will hold its data. The directory is
    further sub-divided in multiple directories for specific use
    cases (e.g. cache, keyring, static, media, logs, templates, etc.).
    Defaults to the "data" sub-directory in the debusine
    base directory (where the code lives).

:py:data:`MEDIA_URL`
    URL that handles the media served from MEDIA_ROOT. Make sure to use a
    trailing slash.
    Examples: "http://example.com/media/", "http://media.example.com/"
    Defaults to "/media/".

:py:data:`STATIC_URL`
    URL prefix for static files.
    Example: "http://example.com/static/", "http://static.example.com/"
    Defaults to "/static/"

Some settings have default values which are computed dynamically from
other settings. Those settings can also be overridden. Here's the list
of those settings.

py:data:`STATIC_ROOT`
    Absolute path to the directory static files should be collected to.
    Don't put anything in this directory yourself; store your static files
    in apps' "static/" subdirectories and in STATICFILES_DIRS. Defaults
    to the "static" sub-directory of :py:data:`DEBUSINE_DATA_PATH`.

:py:data:`MEDIA_ROOT`
    Absolute filesystem path to the directory that will hold user-uploaded
    files. Defaults to the "media" sub-directory of
    :py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_CACHE_DIRECTORY`
    This directory is used to store the locally cached resources.
    Any Debusine app should be able to use this directory to store
    its caches. For example, it is used to store the APT cache of repository
    information and the cache of retrieved Web resources.
    Defaults to the "cache" sub-directory of
    :py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_LOG_DIRECTORY`
    This directory will hold log files generated by debusine.
    Defaults to the "logs" sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_TEMPLATE_DIRECTORY`
    This directory can hold custom templates that will override the
    templates supplied by debusine. Defaults to the "templates"
    sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_TOKENS_DIRECTORY`
    This directory holds tokens for internal use by Debusine. Defaults to
    the "tokens" sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_UPLOAD_DIRECTORY`
    This directory temporarily holds files being uploaded to debusine.
    Defaults to the "uploads" sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_STORE_DIRECTORY`
    This directory is used by the default LocalFileBackend. Defaults to the
    "store" sub-directory of py:data:`DEBUSINE_DATA_PATH`.

:py:data:`DEBUSINE_FALLBACK_ARCHITECTURE`
    Architecture to use for tasks that require one but do not specify one. It
    defaults to ``amd64``.

More settings:

"""

import datetime as dt
import os
import os.path
import socket
from os.path import dirname
from typing import Any, TYPE_CHECKING

import django
from django.core.exceptions import ImproperlyConfigured

from debusine.django.django_utils import in_test_suite

if TYPE_CHECKING:
    from nacl.public import PrivateKey
else:
    PrivateKey = object

if django.VERSION < (4, 2):
    raise ImproperlyConfigured("Debusine needs Django >= 4.2")

# Django's debug mode, never enable this in production
DEBUG = False

BASE_DIR = dirname(dirname(dirname(dirname(__file__))))
DEBUSINE_DATA_PATH = os.path.join(BASE_DIR, 'data')

DEBUSINE_CACHE_DIRECTORY: str
DEBUSINE_TEMPLATE_DIRECTORY: str
DEBUSINE_LOG_DIRECTORY: str
DEBUSINE_UPLOAD_DIRECTORY: str
DEBUSINE_STORE_DIRECTORY: str
DEBUSINE_TOKENS_DIRECTORY: str

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'Etc/UTC'

# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True

# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
MEDIA_URL = '/media/'

# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
}

if in_test_suite():
    STORAGES["staticfiles"] = {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
    }
else:
    STORAGES["staticfiles"] = {
        "BACKEND": "django.contrib.staticfiles.storage"
        ".ManifestStaticFilesStorage"
    }

# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
]


STATICFILES_DIRS = [
    ("vendor/bootstrap5", "/usr/share/javascript/bootstrap5"),
    ("vendor/select2.js", "/usr/share/javascript/select2.js"),
    ("vendor/sortable", "/usr/share/javascript/sortable"),
    ("vendor/jquery", "/usr/share/javascript/jquery"),
]

# Overridden in production.py by content of /var/lib/debusine/server/key
SECRET_KEY = (
    "django-insecure-i77$9ld7x1%h1&_xpdjwy7i-jjaox@-ybtpe#g&8gw@yl4-1%&"
)

# Templating rules
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                # 'django.template.context_processors.i18n',
                # 'django.template.context_processors.media',
                'django.template.context_processors.static',
                # 'django.template.context_processors.tz',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'debusine.web.context_processors.server_info',
                'debusine.web.context_processors.application_context',
            ]
        },
    }
]

MIDDLEWARE: list[str] = [
    'django_prometheus.middleware.PrometheusBeforeMiddleware',
    'debusine.server.middlewares.virtual_hosts.VirtualHostMiddleware',
    # Introduces correct scoping for contextvars
    'debusine.server.middlewares.context.ContextMiddleware',
    'debusine.server.middlewares.headers.HeadersMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'debusine.server.middlewares.scopes.ScopeMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # Needs to stay right after AuthenticationMiddleware
    'debusine.server.signon.middleware.SignonMiddleware',
    'debusine.server.middlewares.scopes.AuthorizationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    # Disabled to allow rendering in iframes
    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django_prometheus.middleware.PrometheusAfterMiddleware',
]

AUTHENTICATION_BACKENDS = [
    'debusine.server.signon.auth.SignonAuthBackend',
]

LOGIN_REDIRECT_URL = "homepage:homepage"

# AUTHENTICATION_BACKENDS = [
#    'django_email_accounts.auth.UserEmailBackend',
# ]
#
# AUTH_USER_MODEL = 'accounts.User'

ROOT_URLCONF = 'debusine.project.urls'

# Python dotted path to the ASGI application used by Django's runserver.
ASGI_APPLICATION = "debusine.project.asgi.application"

# This is required to make request.is_secure() work behind a TLS-terminating
# proxy.
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

INSTALLED_APPS: list[str] = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_celery_results',
    'django_prometheus',
    'channels',
    'pgtrigger',
    'rest_framework',
    'debusine.db',
    'debusine.server',
    'debusine.web',
]

# This will need to be tightened up later.
PAGE_CLASS = 'DEFAULT_PAGINATION_CLASS'
REST_FRAMEWORK = {
    PAGE_CLASS: 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    "EXCEPTION_HANDLER": "debusine.server.exceptions."
    "debusine_exception_handler",
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "debusine.server.views.auth.DebusineTokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}

# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': (
            'django.contrib.auth.password_validation.'
            'UserAttributeSimilarityValidator'
        )
    },
    {
        'NAME': (
            'django.contrib.auth.password_validation.MinimumLengthValidator'
        )
    },
    {
        'NAME': (
            'django.contrib.auth.password_validation.CommonPasswordValidator'
        )
    },
    {
        'NAME': (
            'django.contrib.auth.password_validation.NumericPasswordValidator'
        )
    },
]

# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '%(asctime)s [%(module)s/%(process)d/%(thread)d] '
            + '%(levelname)s: %(message)s'
        },
        'standard': {
            'format': '%(asctime)s %(process)d %(levelname)s: %(message)s'
        },
        'simple': {'format': '%(asctime)s %(levelname)s: %(message)s'},
        'access': {
            '()': 'uvicorn.logging.AccessFormatter',
            'fmt': '%(client_addr)s - "%(request_line)s" %(status_code)s',
        },
    },
    'filters': {
        'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse'},
        'require_debug_true': {'()': 'django.utils.log.RequireDebugTrue'},
        'suppress_db_errors': {
            '()': 'debusine.django.log_filters.SuppressDatabaseErrors'
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
            'filters': ['require_debug_true'],
        },
        'access': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'access',
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false', 'suppress_db_errors'],
            'class': 'django.utils.log.AdminEmailHandler',
        },
        'null': {'level': 'DEBUG', 'class': 'logging.NullHandler'},
        'mail.log': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'mail.log',
            'encoding': 'utf-8',
            'formatter': 'standard',
            'when': 'W0',
            'backupCount': 52,
        },
        'tasks.log': {
            'level': 'INFO',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'tasks.log',
            'encoding': 'utf-8',
            'formatter': 'standard',
            'when': 'W0',
            'backupCount': 52,
        },
        'errors.log': {
            'level': 'WARNING',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'errors.log',
            'encoding': 'utf-8',
            'formatter': 'verbose',
            'when': 'W0',
            'backupCount': 52,
        },
        'debug.log': {
            'level': 'DEBUG',
            'class': 'logging.handlers.TimedRotatingFileHandler',
            'filename': 'debug.log',
            'encoding': 'utf-8',
            'formatter': 'verbose',
            'when': 'W0',
            'backupCount': 52,
        },
    },
    'loggers': {
        'root': {
            'handlers': ['errors.log'],
            'level': 'ERROR',
            'propagate': False,
        },
        'uvicorn': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': False,
        },
        'uvicorn.access': {
            'handlers': ['access'],
            'level': 'INFO',
            'propagate': False,
        },
        'django.request': {
            'handlers': ['errors.log', 'mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'django.security': {
            'handlers': ['errors.log', 'mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'django.security.DisallowedHost': {
            'handlers': ['errors.log'],
            'level': 'ERROR',
            'propagate': False,
        },
        'py.warnings': {'handlers': ['console']},
        'debusine': {
            'handlers': ['debug.log', 'errors.log', 'console'],
            'level': 'DEBUG',
            'propagate': False,
        },
        'debusine.mail': {
            'handlers': ['mail.log', 'mail_admins'],
            'level': 'DEBUG',
            'propagate': True,
        },
        'debusine.tasks': {
            'handlers': ['tasks.log', 'mail_admins'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

AUTH_USER_MODEL = "db.User"

CELERY_BEAT_SCHEDULE = {
    # Run the scheduler once every fifteen seconds, just in case we have a
    # bug where something forgot to request a run of the scheduler when data
    # changed, or in case a scheduler run hit a lock and couldn't make
    # progress.
    "fallback-scheduler": {
        "task": "debusine.server.scheduler.schedule_task",
        "schedule": dt.timedelta(seconds=15),
    },
    "provisioner": {
        "task": "debusine.server.provisioning.provision",
        "schedule": dt.timedelta(minutes=1),
    },
    "worker-pool-scrub": {
        "task": "debusine.server.provisioning.scrub",
        "schedule": dt.timedelta(minutes=15),
    },
}
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "django-db"
CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = {"global_keyprefix": "celery"}
CELERY_RESULT_EXTENDED = True
CELERY_TASK_ROUTES = {
    "debusine.server.scheduler.schedule_task": {"queue": "scheduler"},
    "debusine.server.provisioning.provision": {"queue": "provisioner"},
    "debusine.server.provisioning.scrub": {"queue": "provisioner"},
}
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
# Control memory leaks.
CELERY_WORKER_MAX_MEMORY_PER_CHILD = 512 * 1024  # in KiB, so 512 MiB

PROMETHEUS_REDIS_URL = "redis://localhost:6379/1"

EMAIL_TIMEOUT = 5

PGTRANSACTION_RETRY = 3

# https://github.com/django-commons/django-prometheus/issues/281
PROMETHEUS_EXPORT_MIGRATIONS = False

# *_created Prometheus metrics are useless to us.  Disable them.
os.environ.setdefault("PROMETHEUS_DISABLE_CREATED_SERIES", "1")

# === Debusine specific settings ===

# The fully qualified domain name for the Debusine deployment
DEBUSINE_FQDN: str = socket.getfqdn()

# URL scheme to use to construct Debusine URLs
DEBUSINE_URL_SCHEME: str = "https"

# The fully qualified domain name for Debian-format archives hosted by this
# deployment
DEBUSINE_DEBIAN_ARCHIVE_FQDN: str | list[str] = f"deb.{DEBUSINE_FQDN}"

# URL scheme to use to construct debian archive URLs
DEBUSINE_DEBIAN_ARCHIVE_URL_SCHEME: str = "https"

# Default CHANNEL_LAYERS pointing at a Redis local configuration
CHANNEL_LAYERS: dict[str, dict[str, Any]] = {
    "default": {
        "BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
        "CONFIG": {"hosts": ["redis://localhost:6379"]},
    }
}

# A default contact email
DEBUSINE_CONTACT_EMAIL = f'owner@{DEBUSINE_FQDN}'

# Name of the default scope to use for legacy web requests and API calls that
# are unaware of scopes
DEBUSINE_DEFAULT_SCOPE = 'debusine'
# Name of the default workspace to use for migrations, API calls that do not
# specify the workspace, management commands, and so on.
DEBUSINE_DEFAULT_WORKSPACE = "System"

# Encryption keys used by the signing service to encrypt private keys; the
# first in the list is used for newly-encrypted private keys, while the
# others may also be used for decryption
DEBUSINE_SIGNING_PRIVATE_KEYS: list[PrivateKey] = []

# Allow signing objects that declare a trust chain to these certificate
# fingerprints.  See
# https://wiki.debian.org/SecureBoot/Discussion#Describing_the_trust_chain.
DEBUSINE_SIGNING_TRUSTED_CERTS: list[str] = []

# Architecture to use for tasks that require one but do not specify one.
DEBUSINE_FALLBACK_ARCHITECTURE: str = "amd64"

# The lambda functions are evaluated at the end of the settings import
# logic. They provide default values to settings which have not yet been
# set (neither above nor in local.py).
_COMPUTE_DEFAULT_SETTINGS = (
    (
        'ALLOWED_HOSTS',
        lambda t: (
            [t['DEBUSINE_FQDN']]
            + (
                t['DEBUSINE_DEBIAN_ARCHIVE_FQDN']
                if isinstance(t['DEBUSINE_DEBIAN_ARCHIVE_FQDN'], list)
                else [t['DEBUSINE_DEBIAN_ARCHIVE_FQDN']]
            )
            + [
                'localhost',
                '127.0.0.1',
            ]
        ),
    ),
    (
        'DEBUSINE_DEBIAN_ARCHIVE_PRIMARY_FQDN',
        lambda t: (
            t['DEBUSINE_DEBIAN_ARCHIVE_FQDN'][0]
            if isinstance(t['DEBUSINE_DEBIAN_ARCHIVE_FQDN'], list)
            else t['DEBUSINE_DEBIAN_ARCHIVE_FQDN']
        ),
    ),
    ('ADMINS', lambda t: (('Debusine Admins', t['DEBUSINE_CONTACT_EMAIL']),)),
    ('SERVER_EMAIL', lambda t: t['DEBUSINE_CONTACT_EMAIL']),
    ('DEFAULT_FROM_EMAIL', lambda t: t['DEBUSINE_CONTACT_EMAIL']),
    ('STATIC_ROOT', lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'static')),
    ('MEDIA_ROOT', lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'media')),
    (
        'DEBUSINE_CACHE_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'cache'),
    ),
    (
        'DEBUSINE_TEMPLATE_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'templates'),
    ),
    (
        'DEBUSINE_LOG_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'logs'),
    ),
    (
        'DEBUSINE_UPLOAD_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'uploads'),
    ),
    (
        'DEBUSINE_STORE_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'store'),
    ),
    (
        'DEBUSINE_TOKENS_DIRECTORY',
        lambda t: os.path.join(t['DEBUSINE_DATA_PATH'], 'tokens'),
    ),
)


def compute_default_settings(target: dict[str, Any]) -> None:
    """
    Dynamically generate some default settings.

    There are many settings whose default value depends on another
    setting. They are defined in the _COMPUTE_DEFAULT_SETTINGS dict
    with a function that evaluates them.
    """
    for setting, value in _COMPUTE_DEFAULT_SETTINGS:
        if setting in target:
            continue  # Settings is already defined
        target[setting] = value(target)
    # Extend TEMPLATE_DIRS with our directory
    target['TEMPLATES'][0]['DIRS'].append(target['DEBUSINE_TEMPLATE_DIRECTORY'])
    # Update LOGGING with full paths
    for handler in target['LOGGING']['handlers'].values():
        if 'filename' not in handler or "/" in handler['filename']:
            continue
        handler['filename'] = os.path.join(
            target['DEBUSINE_LOG_DIRECTORY'], handler['filename']
        )
    # Update DATABASES with full paths
    dbconf = target['DATABASES']['default']
    if dbconf['ENGINE'] == 'django.db.backends.sqlite3':
        if '/' not in dbconf['NAME']:
            dbconf['NAME'] = os.path.join(
                target['DEBUSINE_DATA_PATH'], dbconf['NAME']
            )
        if (
            'TEST' in dbconf
            and 'NAME' in dbconf['TEST']
            and '/' not in dbconf['TEST']['NAME']
        ):
            dbconf['TEST']['NAME'] = os.path.join(
                target['DEBUSINE_DATA_PATH'], dbconf['TEST']['NAME']
            )


LOGGING_CONFIG = "debusine.django.django_utils.test_data_override"
