"""
Django settings for messages project.

Generated by 'django-admin startproject' using Django 3.1.5.

For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
# pylint: disable=too-many-lines

import logging
import os
import tomllib
from socket import gethostbyname, gethostname

import dj_database_url
import sentry_sdk
from configurations import Configuration, values
from sentry_sdk.integrations.django import DjangoIntegration

from core.utils import JSONValue, OptionalBooleanValue, ThrottleRateValue

logger = logging.getLogger(__name__)

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def get_release():
    """
    Get the current release of the application
    """
    try:
        with open(os.path.join(BASE_DIR, "pyproject.toml"), "rb") as f:
            pyproject_data = tomllib.load(f)
        return pyproject_data["project"]["version"]
    except (FileNotFoundError, KeyError):
        return "NA"  # Default: not available


class Base(Configuration):
    """
    This is the base configuration every configuration (aka environment) should inherit from. It
    is recommended to configure third-party applications by creating a configuration mixins in
    ./configurations and compose the Base configuration with those mixins.

    It depends on an environment variable that SHOULD be defined:

    * DJANGO_SECRET_KEY

    You may also want to override default configuration by setting the following environment
    variables:

    * SENTRY_DSN
    * DB_NAME
    * DB_HOST
    * DB_PASSWORD
    * DB_USER
    """

    DEBUG = False
    USE_SWAGGER = False

    API_VERSION = "v1.0"

    # Admin URL configuration
    ADMIN_URL = values.Value("admin")

    # OpenSearch configuration
    OPENSEARCH_HOSTS = values.ListValue(
        ["http://opensearch:9200"],
        environ_name="OPENSEARCH_URL",
        environ_prefix=None,
    )
    OPENSEARCH_TIMEOUT = values.PositiveIntegerValue(
        20, environ_name="OPENSEARCH_TIMEOUT", environ_prefix=None
    )
    OPENSEARCH_BULK_TIMEOUT = values.PositiveIntegerValue(
        60, environ_name="OPENSEARCH_BULK_TIMEOUT", environ_prefix=None
    )
    OPENSEARCH_BULK_MAX_BYTES = values.PositiveIntegerValue(
        25 * 1024 * 1024,
        environ_name="OPENSEARCH_BULK_MAX_BYTES",
        environ_prefix=None,
    )
    # Number of thread documents (and their child message documents)
    # accumulated before a bulk flush. Lower values reduce per-request
    # cluster pressure (heap, queue depth) at the cost of more round-trips.
    OPENSEARCH_BULK_CHUNK_SIZE = values.PositiveIntegerValue(
        50,
        environ_name="OPENSEARCH_BULK_CHUNK_SIZE",
        environ_prefix=None,
    )
    # Transport-level retry budget for the OpenSearch client. The
    # opensearch-py transport already retries on 502/503/504 (its
    # ``DEFAULT_RETRY_ON_STATUS``) — this just exposes the count so we
    # can lift it above the library default of 3 if needed. Whatever
    # bubbles up through this budget is wrapped as
    # ``TransientTransportError`` and handed to Celery autoretry.
    OPENSEARCH_MAX_RETRIES = values.PositiveIntegerValue(
        3,
        environ_name="OPENSEARCH_MAX_RETRIES",
        environ_prefix=None,
    )
    OPENSEARCH_INDEX_THREADS = values.BooleanValue(
        True, environ_name="OPENSEARCH_INDEX_THREADS", environ_prefix=None
    )
    OPENSEARCH_CA_CERTS = values.Value(
        None, environ_name="OPENSEARCH_CA_CERTS", environ_prefix=None
    )
    # Interval (seconds) at which the Celery Beat task drains the Redis
    # coalescing buffers (reindex + delete) and enqueues bulk thread tasks.
    # Longer intervals reduce Celery/OpenSearch load at the cost of search
    # result staleness for recent writes.
    SEARCH_REINDEX_TASKS_INTERVAL = values.PositiveIntegerValue(
        30,
        environ_name="SEARCH_REINDEX_TASKS_INTERVAL",
        environ_prefix=None,
    )
    # Maximum number of thread / message IDs handed to a single
    # ``bulk_*_task`` call. Each batch becomes one Celery task, so the
    # value is the unit of parallelism, retry granularity and worker
    # occupation for catch-up flows. Lower means more, shorter tasks
    # (better parallelism, cheaper retries); higher means fewer, longer
    # tasks (less broker chatter but worse failure isolation).
    SEARCH_FLUSH_BATCH_SIZE = values.PositiveIntegerValue(
        1000,
        environ_name="SEARCH_FLUSH_BATCH_SIZE",
        environ_prefix=None,
    )
    # Maximum number of ``bulk_*_task`` calls a single Beat tick is
    # allowed to enqueue, shared across the three handoffs (reindex /
    # thread-delete / message-delete). Bounds catch-up bursts so a huge
    # backlog is spread across several ticks rather than flooding the
    # broker in one go. Effective per-tick capacity is roughly
    # ``SEARCH_FLUSH_BATCH_SIZE * SEARCH_FLUSH_MAX_BATCHES`` IDs.
    SEARCH_FLUSH_MAX_BATCHES = values.PositiveIntegerValue(
        10,
        environ_name="SEARCH_FLUSH_MAX_BATCHES",
        environ_prefix=None,
    )

    # Upload limits
    DATA_UPLOAD_MAX_MEMORY_SIZE = values.PositiveIntegerValue(
        2621440, environ_name="DATA_UPLOAD_MAX_MEMORY_SIZE", environ_prefix=None
    )  # Default 2.5MB

    # Email size limits (using binary MiB for actual limits)
    MAX_INCOMING_EMAIL_SIZE = values.PositiveIntegerValue(
        10 * 1024 * 1024,  # 10 MiB
        environ_name="MAX_INCOMING_EMAIL_SIZE",
        environ_prefix=None,
    )

    MAX_OUTGOING_ATTACHMENT_SIZE = values.PositiveIntegerValue(
        20 * 1024 * 1024,  # 20 MiB
        environ_name="MAX_OUTGOING_ATTACHMENT_SIZE",
        environ_prefix=None,
    )

    MAX_TEMPLATE_IMAGE_SIZE = values.PositiveIntegerValue(
        2 * 1024 * 1024,  # 2 MiB
        environ_name="MAX_TEMPLATE_IMAGE_SIZE",
        environ_prefix=None,
    )

    MAX_OUTGOING_BODY_SIZE = values.PositiveIntegerValue(
        5 * 1024 * 1024,  # 5 MiB
        environ_name="MAX_OUTGOING_BODY_SIZE",
        environ_prefix=None,
    )

    # Maximum total recipients (to + cc + bcc) allowed per message for the entire system
    MAX_RECIPIENTS_PER_MESSAGE = values.PositiveIntegerValue(
        500, environ_name="MAX_RECIPIENTS_PER_MESSAGE", environ_prefix=None
    )

    # Thread events
    # Time window (in seconds) during which a ThreadEvent can be edited or
    # deleted after creation. Set to 0 to disable the restriction and allow
    # edits indefinitely.
    MAX_THREAD_EVENT_EDIT_DELAY = values.PositiveIntegerValue(
        60 * 60,  # 1 hour in seconds
        environ_name="MAX_THREAD_EVENT_EDIT_DELAY",
        environ_prefix=None,
    )

    # Throttling - limits external recipients per mailbox/maildomain per time period
    # Format: "count/period" where period is minute, hour, or day. None to disable.
    THROTTLE_MAILBOX_OUTBOUND_EXTERNAL_RECIPIENTS = ThrottleRateValue(
        None,
        environ_name="THROTTLE_MAILBOX_OUTBOUND_EXTERNAL_RECIPIENTS",
        environ_prefix=None,
    )
    THROTTLE_MAILDOMAIN_OUTBOUND_EXTERNAL_RECIPIENTS = ThrottleRateValue(
        None,
        environ_name="THROTTLE_MAILDOMAIN_OUTBOUND_EXTERNAL_RECIPIENTS",
        environ_prefix=None,
    )

    # Autoreply rate limit: max autoreplies per sender per mailbox per time period
    # Format: "count/period" where period is minute, hour, or day. None to disable.
    THROTTLE_AUTOREPLY_PER_SENDER = ThrottleRateValue(
        "1/day",
        environ_name="THROTTLE_AUTOREPLY_PER_SENDER",
        environ_prefix=None,
    )

    # Image Proxy
    IMAGE_PROXY_ENABLED = values.BooleanValue(
        False, environ_name="IMAGE_PROXY_ENABLED", environ_prefix=None
    )
    IMAGE_PROXY_MAX_SIZE = values.PositiveIntegerValue(
        5 * 1024 * 1024,  # 5 MiB
        environ_name="IMAGE_PROXY_MAX_SIZE",
        environ_prefix=None,
    )
    IMAGE_PROXY_CACHE_TTL = values.PositiveIntegerValue(
        60 * 60 * 24 * 30,  # 30 days in seconds
        environ_name="IMAGE_PROXY_CACHE_TTL",
        environ_prefix=None,
    )

    # Security
    ALLOWED_HOSTS = values.ListValue([])
    SECRET_KEY = values.Value(None)
    SERVER_TO_SERVER_API_TOKENS = values.ListValue([])

    # SSRF allowlist: hostnames that bypass the private/internal-IP checks in
    # ``core.services.ssrf``. Needed for trusted destinations that only resolve
    # to a private address from inside the platform network — e.g. app-to-app
    # traffic on a PaaS where a public hostname resolves onto an internal 10.x
    # overlay. Entries are exact, case-insensitive hostnames (not CIDRs); keep
    # this list as narrow as possible, since each entry is a deliberate hole in
    # the SSRF protection.
    SSRF_ALLOWED_HOSTS = values.ListValue(
        [], environ_name="SSRF_ALLOWED_HOSTS", environ_prefix=None
    )

    USE_X_FORWARDED_FOR = values.BooleanValue(
        default=False, environ_name="USE_X_FORWARDED_FOR", environ_prefix=None
    )

    # Application definition
    ROOT_URLCONF = "messages.urls"
    WSGI_APPLICATION = "messages.wsgi.application"

    # Database
    DATABASES = {
        "default": dj_database_url.config()
        if os.environ.get("DATABASE_URL")
        else {
            "ENGINE": values.Value(
                "django_prometheus.db.backends.postgresql",
                environ_name="DB_ENGINE",
                environ_prefix=None,
            ),
            "NAME": values.Value(
                "messages", environ_name="DB_NAME", environ_prefix=None
            ),
            "USER": values.Value("dbuser", environ_name="DB_USER", environ_prefix=None),
            "PASSWORD": values.Value(
                "dbpass", environ_name="DB_PASSWORD", environ_prefix=None
            ),
            "HOST": values.Value(
                "localhost", environ_name="DB_HOST", environ_prefix=None
            ),
            "PORT": values.Value(5432, environ_name="DB_PORT", environ_prefix=None),
        }
    }
    DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

    # Model custom attributes schema
    SCHEMA_CUSTOM_ATTRIBUTES_USER = JSONValue(
        {}, environ_name="SCHEMA_CUSTOM_ATTRIBUTES_USER", environ_prefix=None
    )
    SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN = JSONValue(
        {}, environ_name="SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN", environ_prefix=None
    )

    # Denylist of local parts for personal mailboxes.
    # Any personal mailbox whose local_part exactly matches one of these values
    # (case-insensitive) will be rejected.
    MESSAGES_MAILBOX_LOCALPART_DENYLIST_PERSONAL = values.ListValue(
        [],
        environ_name="MESSAGES_MAILBOX_LOCALPART_DENYLIST_PERSONAL",
        environ_prefix=None,
    )

    DATA_DIR = values.Value(
        "/data", environ_name="DJANGO_DATA_DIR", environ_prefix=None
    )

    # Static files (CSS, JavaScript, Images)
    STATIC_URL = "/static/"
    STATIC_ROOT = os.path.join(DATA_DIR, "static")
    MEDIA_URL = "/media/"
    MEDIA_ROOT = os.path.join(DATA_DIR, "media")

    SITE_ID = 1

    STORAGES = {
        "default": {
            "BACKEND": "storages.backends.s3.S3Storage",
        },
        "staticfiles": {
            "BACKEND": values.Value(
                "whitenoise.storage.CompressedManifestStaticFilesStorage",
                environ_name="STORAGES_STATICFILES_BACKEND",
            ),
        },
        "message-imports": {
            "BACKEND": "storages.backends.s3.S3Storage",
            "OPTIONS": {
                "endpoint_url": values.Value(
                    environ_name="STORAGE_MESSAGE_IMPORTS_ENDPOINT_URL",
                    environ_prefix=None,
                ),
                "bucket_name": values.Value(
                    environ_name="STORAGE_MESSAGE_IMPORTS_BUCKET_NAME",
                    environ_prefix=None,
                ),
                "access_key": values.Value(
                    environ_name="STORAGE_MESSAGE_IMPORTS_ACCESS_KEY",
                    environ_prefix=None,
                ),
                "secret_key": values.Value(
                    environ_name="STORAGE_MESSAGE_IMPORTS_SECRET_KEY",
                    environ_prefix=None,
                ),
                "region_name": values.Value(
                    environ_name="STORAGE_MESSAGE_IMPORTS_REGION_NAME",
                    environ_prefix=None,
                ),
                "querystring_expire": values.PositiveIntegerValue(
                    environ_name="STORAGE_MESSAGE_IMPORTS_EXPIRE_POLICY",
                    environ_prefix=None,
                ),
            },
        },
        "message-blobs": {
            "BACKEND": "storages.backends.s3.S3Storage",
            "OPTIONS": {
                "endpoint_url": values.Value(
                    environ_name="STORAGE_MESSAGE_BLOBS_ENDPOINT_URL",
                    environ_prefix=None,
                ),
                "bucket_name": values.Value(
                    environ_name="STORAGE_MESSAGE_BLOBS_BUCKET_NAME",
                    environ_prefix=None,
                ),
                "access_key": values.Value(
                    environ_name="STORAGE_MESSAGE_BLOBS_ACCESS_KEY",
                    environ_prefix=None,
                ),
                "secret_key": values.Value(
                    environ_name="STORAGE_MESSAGE_BLOBS_SECRET_KEY",
                    environ_prefix=None,
                ),
                "region_name": values.Value(
                    environ_name="STORAGE_MESSAGE_BLOBS_REGION_NAME",
                    environ_prefix=None,
                ),
            },
        },
    }
    # MDA settings
    # No default on purpose: the MTA-to-MDA channel is authenticated solely by
    # an HS256 JWT signed with this shared secret, so a hardcoded fallback would
    # be internet-spoofable in production. The development value is supplied via
    # deploy/env/backend.defaults (and the matching mta-in.defaults), the
    # same way DJANGO_SECRET_KEY is handled. Unset → None → all MTA auth fails
    # closed.
    MDA_API_SECRET = values.Value(
        None, environ_name="MDA_API_SECRET", environ_prefix=None
    )

    # Product name advertised in the outbound X-Mailer header (the running release
    # is appended). See compose_and_sign_mime.
    MDA_HEADER_XMAILER = "ST Messages"

    # Default CalDAV server settings (optional). Enables calendar features
    # for every mailbox that has not configured its own per-mailbox CalDAV
    # Channel — users can override the integration by pointing a Channel at
    # a CalDAV server of their choice. These ``DEFAULT`` values are the
    # fallback applied when no such Channel exists.
    #
    # Trust model — IMPORTANT
    # -----------------------
    # ``CALDAV_DEFAULT_URL`` is the CalDAV server root.
    # ``CALDAV_DEFAULT_PASSWORD`` is a single secret sent as the HTTP Basic
    # Auth *password* on every outbound request. The Basic Auth *username*
    # is the requesting user's OIDC identity email (``User.email``), which
    # is what providers like suitenumerique/calendars key principals on —
    # NOT the mailbox's ``local_part@domain.name`` (which can diverge from
    # the user's OIDC email).
    #
    # That means the same secret authenticates messages-as-a-service for
    # all users — the CalDAV server is then responsible for whatever
    # per-user authorization it wants to layer on top. Concretely: any
    # user authenticated via OIDC can cause messages to authenticate to
    # the CalDAV server as their OIDC email. The load-bearing safety
    # property is therefore that the OIDC identity provider does not let
    # one human assert another human's ``email`` claim. Operators wiring
    # up this integration must verify that property holds for their IdP
    # configuration (e.g. that ``email`` is a verified claim).
    CALDAV_DEFAULT_URL = values.Value(
        None, environ_name="CALDAV_DEFAULT_URL", environ_prefix=None
    )
    CALDAV_DEFAULT_PASSWORD = values.Value(
        None, environ_name="CALDAV_DEFAULT_PASSWORD", environ_prefix=None
    )
    # Public URL of the default calendar web UI (e.g. a hosted Calendars
    # instance). Used by the mail UI to deep-link into the calendar app so
    # the user can see the event after accepting, or create a calendar when
    # they have none. Like the credentials above, this is the deployment-
    # wide default — a per-mailbox Channel pointing at a different CalDAV
    # provider is not expected to surface a web URL here.
    CALDAV_DEFAULT_WEB_URL = values.Value(
        None, environ_name="CALDAV_DEFAULT_WEB_URL", environ_prefix=None
    )

    # Spam filtering settings

    # Default spam configuration for all mail domains, overrideable per mail
    # domain in custom_settings. Recognised keys include:
    #   rspamd_url / rspamd_auth : rspamd /checkv2 endpoint + optional auth header
    #   trusted_relays           : int, how many sender-side upstream Received
    #                              blocks to trust for header-based rules
    #                              (default 0 — trust only the Received block our
    #                              own MTA prepends; a sender can forge any block
    #                              above that, so only raise this to the number of
    #                              relay hops you actually operate)
    #   rules                    : list of hardcoded header-match spam rules
    #   inbound_auth             : sender authentication backend — one of
    #                              "native", "rspamd", "authentication-results",
    #                              or None/absent to disable. See
    #                              core.mda.inbound_auth for semantics.
    SPAM_CONFIG = values.DictValue({}, environ_name="SPAM_CONFIG", environ_prefix=None)

    # MTA settings

    # "direct" or "relay"
    MTA_OUT_MODE = values.Value(
        "direct", environ_name="MTA_OUT_MODE", environ_prefix=None
    )

    # SMTP settings for the direct outbound MTA
    MTA_OUT_DIRECT_PROXIES = values.ListValue(
        [], environ_name="MTA_OUT_DIRECT_PROXIES", environ_prefix=None
    )
    MTA_OUT_DIRECT_PORT = values.PositiveIntegerValue(
        25, environ_name="MTA_OUT_DIRECT_PORT", environ_prefix=None
    )

    # SMTP settings for external SMTP servers, if MTA_OUT_MODE="relay"
    MTA_OUT_RELAY_HOST = values.Value(
        None, environ_name="MTA_OUT_RELAY_HOST", environ_prefix=None
    )
    MTA_OUT_RELAY_USERNAME = values.Value(
        None, environ_name="MTA_OUT_RELAY_USERNAME", environ_prefix=None
    )
    MTA_OUT_RELAY_PASSWORD = values.Value(
        None, environ_name="MTA_OUT_RELAY_PASSWORD", environ_prefix=None
    )

    # SMTP TLS policy for both direct (MX) and relay paths. Subset of
    # https://www.postfix.org/postconf.5.html#smtp_tls_security_level :
    #   - "none"  : never attempt STARTTLS.
    #   - "may"   : opportunistic TLS, no cert verification (Postfix-aligned).
    #               Suitable default for direct MX delivery, where many public
    #               MXes serve mismatched or self-signed certs.
    #   - "secure": mandatory TLS + CA chain + hostname check; defers on
    #               failure. Use this when running against a controlled relay
    #               with a valid cert (SMTP AUTH credentials are sent inside
    #               the TLS tunnel, so an unverified peer is a MITM risk).
    MTA_OUT_SMTP_TLS_SECURITY_LEVEL = values.Value(
        "may", environ_name="MTA_OUT_SMTP_TLS_SECURITY_LEVEL", environ_prefix=None
    )

    # Test domain settings
    MESSAGES_TESTDOMAIN = values.Value(
        None, environ_name="MESSAGES_TESTDOMAIN", environ_prefix=None
    )
    MESSAGES_TESTDOMAIN_MAPPING_BASEDOMAIN = values.Value(
        None,
        environ_name="MESSAGES_TESTDOMAIN_MAPPING_BASEDOMAIN",
        environ_prefix=None,
    )
    MESSAGES_ACCEPT_ALL_EMAILS = values.BooleanValue(
        default=False,
        environ_name="MESSAGES_ACCEPT_ALL_EMAILS",
        environ_prefix=None,
    )

    # IMAP importer settings
    IMAP_TIMEOUT = values.PositiveIntegerValue(
        60, environ_name="IMAP_TIMEOUT", environ_prefix=None
    )
    IMAP_MAX_RETRIES = values.PositiveIntegerValue(
        3, environ_name="IMAP_MAX_RETRIES", environ_prefix=None
    )

    # Self-check settings
    MESSAGES_SELFCHECK_FROM = values.Value(
        None,
        environ_name="MESSAGES_SELFCHECK_FROM",
        environ_prefix=None,
    )
    MESSAGES_SELFCHECK_TO = values.Value(
        None,
        environ_name="MESSAGES_SELFCHECK_TO",
        environ_prefix=None,
    )
    MESSAGES_SELFCHECK_SECRET = values.Value(
        "self-check-secret-for-dev",
        environ_name="MESSAGES_SELFCHECK_SECRET",
        environ_prefix=None,
    )
    MESSAGES_SELFCHECK_INTERVAL = values.PositiveIntegerValue(
        600,  # 10 minutes
        environ_name="MESSAGES_SELFCHECK_INTERVAL",
        environ_prefix=None,
    )
    MESSAGES_SELFCHECK_TIMEOUT = values.PositiveIntegerValue(
        60,  # 60 seconds
        environ_name="MESSAGES_SELFCHECK_TIMEOUT",
        environ_prefix=None,
    )

    MESSAGES_SELFCHECK_WEBHOOK_URL = values.Value(
        None,
        environ_name="MESSAGES_SELFCHECK_WEBHOOK_URL",
        environ_prefix=None,
    )

    MESSAGES_SELFCHECK_SENTRY_MONITOR_SLUG = values.Value(
        None,
        environ_name="MESSAGES_SELFCHECK_SENTRY_MONITOR_SLUG",
        environ_prefix=None,
    )

    # Public base URL of this Messages instance, i.e. the origin that serves
    # both the API and the web app (e.g. ``https://messages-public-url.example.com``).
    # Instance-wide, not feature-specific: anything that has to emit an
    # absolute link back to ourselves should read this. Today the only
    # consumer is outbound webhooks, which send it as the ``X-StMsg-Instance``
    # header so a receiver can build callback API URLs from the ``*-Id``
    # headers; the header is omitted when this is unset.
    INSTANCE_URL = values.Value(
        None,
        environ_name="INSTANCE_URL",
        environ_prefix=None,
    )

    # Manual retry settings
    MESSAGES_MANUAL_RETRY_MAX_AGE = values.PositiveIntegerValue(
        7 * 24 * 60 * 60,  # 7 days in seconds
        environ_name="MESSAGES_MANUAL_RETRY_MAX_AGE",
        environ_prefix=None,
    )

    # How long an inbound message is deferred (held and retried) when a
    # processing step keeps failing, before the pipeline stops holding it. A
    # message whose step keeps failing (rspamd unreachable, a blocking
    # webhook erroring) is re-attempted every 5 minutes up to this age, then
    # delivered anyway — flagged ``X-StMsg-Processing-Failed`` — so mail is
    # never lost, only delayed. A parse/create failure that never clears is
    # marked abandoned at this age instead. Operators trade delivery latency
    # (longer = more time for a flaky dependency to recover before we deliver
    # unchecked) against how long unchecked mail can sit held during an
    # outage. See ``DEFERRAL_MAX_AGE`` in ``core/mda/inbound_pipeline.py``.
    MESSAGES_INBOUND_DEFERRAL_MAX_AGE = values.PositiveIntegerValue(
        48 * 60 * 60,  # 48 hours in seconds
        environ_name="MESSAGES_INBOUND_DEFERRAL_MAX_AGE",
        environ_prefix=None,
    )

    # Default compression for new blobs.
    # Format: "<algo>" or "<algo>:<level>". Examples: "none", "zstd", "zstd:7".
    MESSAGES_BLOBS_COMPRESS = values.Value(
        "zstd:7", environ_name="MESSAGES_BLOBS_COMPRESS", environ_prefix=None
    )

    # Blob encryption keys (dict mapping key_id -> entry). Each entry must
    # be ``{"algo": "<name>", "secret": "<32+ chars>"}`` — no shorthand,
    # so the operator's algo choice is always explicit. Add ``"active":
    # true`` to exactly one entry to make it the key new blobs encrypt
    # with; entries without ``active`` (or with ``active=false``) stay
    # readable for legacy ciphertext but no longer encrypt. The secret is
    # SHA-256'd into a 32-byte AEAD key; operators must supply
    # high-entropy values (e.g. ``openssl rand -base64 32``).
    # key_id=0 in DB means a row was never encrypted; key_id>=1 refers to
    # a dict entry. Works for both PostgreSQL and object storage blobs.
    # Examples:
    #   {} — fully off (no keys, no decrypt either)
    #   {"1": {"algo": "aes-gcm", "secret": "<...>"}} — read-only legacy
    #     mode (decrypt key-1 blobs but encrypt new blobs as key_id=0)
    #   {"1": {"algo": "aes-gcm", "secret": "<...>", "active": true}} —
    #     encryption on, key 1 encrypts new blobs
    MESSAGES_BLOBS_ENCRYPT_KEYS = JSONValue(
        {}, environ_name="MESSAGES_BLOBS_ENCRYPT_KEYS", environ_prefix=None
    )
    # When True, ``Blob.get_content`` re-hashes the decompressed plaintext
    # and raises ``ValueError`` if it doesn't match ``blob.sha256``. Costs
    # one SHA-256 over the plaintext per read. Defense-in-depth against
    # storage-tier corruption and content substitution; for encrypted
    # blobs the AAD-bound auth tag already catches swap attacks, so this
    # is most useful for ``key_id=0`` (plaintext-stored) blobs.
    MESSAGES_BLOBS_VERIFY_HASH = values.BooleanValue(
        default=False,
        environ_name="MESSAGES_BLOBS_VERIFY_HASH",
        environ_prefix=None,
    )

    # Tiered Storage offload settings
    # Master switch - must be explicitly enabled
    MESSAGES_BLOBS_OFFLOAD_ENABLED = values.BooleanValue(
        default=False,
        environ_name="MESSAGES_BLOBS_OFFLOAD_ENABLED",
        environ_prefix=None,
    )
    # Offload blobs older than this many seconds (0 = immediately)
    MESSAGES_BLOBS_OFFLOAD_DELAY = values.PositiveIntegerValue(
        24 * 60 * 60,  # 1 day in seconds
        environ_name="MESSAGES_BLOBS_OFFLOAD_DELAY",
        environ_prefix=None,
    )
    # Minimum blob size in bytes to offload (0 = all)
    MESSAGES_BLOBS_OFFLOAD_MIN_SIZE = values.PositiveIntegerValue(
        default=0, environ_name="MESSAGES_BLOBS_OFFLOAD_MIN_SIZE", environ_prefix=None
    )

    # Django fernet encrypted fields settings
    # Can be a list for key rotation: ['new_key', 'old_key']
    SALT_KEY = values.ListValue([], environ_name="SALT_KEY", environ_prefix=None)

    # DKIM settings
    MESSAGES_DKIM_DEFAULT_SELECTOR = values.Value(
        "stmessages", environ_name="MESSAGES_DKIM_DEFAULT_SELECTOR", environ_prefix=None
    )

    # DKIM verification settings
    MESSAGES_DKIM_VERIFY_OUTGOING = values.BooleanValue(
        default=False,
        environ_name="MESSAGES_DKIM_VERIFY_OUTGOING",
        environ_prefix=None,
    )

    # Block outgoing messages when SPF includes are not correctly set up
    MESSAGES_SPF_CHECK_OUTGOING = values.BooleanValue(
        default=False,
        environ_name="MESSAGES_SPF_CHECK_OUTGOING",
        environ_prefix=None,
    )

    # Deliver mailbox-to-mailbox mail through the internal inbound pipeline
    # (the fast path) instead of routing it out through the MTA. Set False to
    # force every message — including same-instance recipients — through the
    # external MTA path, e.g. so all mail passes the same scanning/archiving
    # as outbound. Default True keeps the local fast path.
    MESSAGES_ALLOW_INTERNAL_DELIVERY = values.BooleanValue(
        default=True,
        environ_name="MESSAGES_ALLOW_INTERNAL_DELIVERY",
        environ_prefix=None,
    )

    # Technical domain for DNS records (MX, SPF, DKIM hosting)
    MESSAGES_TECHNICAL_DOMAIN = values.Value(
        "localhost", environ_name="MESSAGES_TECHNICAL_DOMAIN", environ_prefix=None
    )

    # DNS record templates (JSON string with {technical_domain} placeholders)
    MESSAGES_DNS_RECORDS = values.Value(
        '[{"target":"","type":"mx","value":"10 mx1.{technical_domain}."},'
        '{"target":"","type":"mx","value":"20 mx2.{technical_domain}."},'
        '{"target":"","type":"txt","value":"v=spf1 include:_spf.{technical_domain} -all"},'
        '{"target":"_dmarc","type":"txt","value":"v=DMARC1; p=reject; adkim=s; aspf=s;"}]',
        environ_name="MESSAGES_DNS_RECORDS",
        environ_prefix=None,
    )

    # DNS Provider settings
    DNS_SCALEWAY_API_TOKEN = values.Value(
        None, environ_name="DNS_SCALEWAY_API_TOKEN", environ_prefix=None
    )
    DNS_SCALEWAY_PROJECT_ID = values.Value(
        None, environ_name="DNS_SCALEWAY_PROJECT_ID", environ_prefix=None
    )
    DNS_SCALEWAY_TTL = values.PositiveIntegerValue(
        3600, environ_name="DNS_SCALEWAY_TTL", environ_prefix=None
    )
    DNS_DEFAULT_PROVIDER = values.Value(
        None, environ_name="DNS_DEFAULT_PROVIDER", environ_prefix=None
    )

    # Media
    AWS_S3_DOMAIN_REPLACE = values.Value(
        environ_name="AWS_S3_DOMAIN_REPLACE", environ_prefix=None
    )
    AWS_S3_ENDPOINT_URL = values.Value(
        environ_name="AWS_S3_ENDPOINT_URL", environ_prefix=None
    )
    AWS_S3_ACCESS_KEY_ID = values.Value(
        environ_name="AWS_S3_ACCESS_KEY_ID", environ_prefix=None
    )
    AWS_S3_SECRET_ACCESS_KEY = values.Value(
        environ_name="AWS_S3_SECRET_ACCESS_KEY", environ_prefix=None
    )
    AWS_S3_REGION_NAME = values.Value(
        environ_name="AWS_S3_REGION_NAME", environ_prefix=None
    )
    AWS_STORAGE_BUCKET_NAME = values.Value(
        "st-messages-media-storage",
        environ_name="AWS_STORAGE_BUCKET_NAME",
        environ_prefix=None,
    )
    AWS_S3_UPLOAD_POLICY_EXPIRATION = values.Value(
        24 * 60 * 60,  # 24h
        environ_name="AWS_S3_UPLOAD_POLICY_EXPIRATION",
        environ_prefix=None,
    )
    AWS_S3_SIGNATURE_VERSION = values.Value(
        "s3v4",
        environ_name="AWS_S3_SIGNATURE_VERSION",
        environ_prefix=None,
    )

    # item images
    ITEM_FILE_MAX_SIZE = values.PositiveIntegerValue(
        5 * (2**30),  # 5GB
        environ_name="ITEM_FILE_MAX_SIZE",
        environ_prefix=None,
    )

    # Internationalization
    # https://docs.djangoproject.com/en/3.1/topics/i18n/

    # Languages
    LANGUAGE_CODE = values.Value("en-us")
    DRF_NESTED_MULTIPART_PARSER = {
        # output of parser is converted to querydict
        # if is set to False, dict python is returned
        "querydict": False,
    }

    # Careful! Languages should be ordered by priority, as this tuple is used to get
    # fallback/default languages throughout the app.
    LANGUAGES = values.SingleNestedTupleValue(
        (
            ("en-us", "English"),
            ("fr-fr", "Français"),
            ("nl-nl", "Nederlands"),
        )
    )

    TIME_ZONE = "UTC"
    USE_I18N = False
    USE_TZ = True

    # Templates
    TEMPLATES = [
        {
            "BACKEND": "django.template.backends.django.DjangoTemplates",
            "DIRS": [os.path.join(BASE_DIR, "templates")],
            "OPTIONS": {
                "context_processors": [
                    "django.contrib.auth.context_processors.auth",
                    "django.contrib.messages.context_processors.messages",
                    "django.template.context_processors.csrf",
                    "django.template.context_processors.debug",
                    "django.template.context_processors.media",
                    "django.template.context_processors.request",
                    "django.template.context_processors.tz",
                ],
                "loaders": [
                    "django.template.loaders.filesystem.Loader",
                    "django.template.loaders.app_directories.Loader",
                ],
            },
        },
    ]

    MIDDLEWARE = [
        "django.middleware.security.SecurityMiddleware",
        "whitenoise.middleware.WhiteNoiseMiddleware",
        "django.contrib.sessions.middleware.SessionMiddleware",
        "django.middleware.clickjacking.XFrameOptionsMiddleware",
        "core.middlewares.CustomCorsMiddleware",
        "django.middleware.common.CommonMiddleware",
        "django.middleware.csrf.CsrfViewMiddleware",
        "django.contrib.auth.middleware.AuthenticationMiddleware",
        "django.contrib.messages.middleware.MessageMiddleware",
    ]

    AUTHENTICATION_BACKENDS = [
        "django.contrib.auth.backends.ModelBackend",
        "core.authentication.backends.OIDCAuthenticationBackend",
    ]

    # Django applications from the highest priority to the lowest
    INSTALLED_APPS = [
        "core",
        "drf_spectacular",
        # Third party apps
        "corsheaders",
        "django_celery_beat",
        "django_celery_results",
        "django_filters",
        "rest_framework",
        # Django
        "django.contrib.admin",
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.postgres",
        "django.contrib.sessions",
        "django.contrib.sites",
        "django.contrib.messages",
        "django.contrib.staticfiles",
    ]

    # Cache
    CACHES = {
        "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
    }

    REST_FRAMEWORK = {
        "DEFAULT_AUTHENTICATION_CLASSES": (
            "mozilla_django_oidc.contrib.drf.OIDCAuthentication",
            "rest_framework.authentication.SessionAuthentication",
        ),
        "DEFAULT_PARSER_CLASSES": [
            "rest_framework.parsers.JSONParser",
            "nested_multipart_parser.drf.DrfNestedParser",
        ],
        "EXCEPTION_HANDLER": "core.api.exception_handler",
        "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
        "PAGE_SIZE": 20,
        "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
        "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
        "DEFAULT_THROTTLE_RATES": {
            "user_list_sustained": values.Value(
                default="180/hour",
                environ_name="API_USERS_LIST_THROTTLE_RATE_SUSTAINED",
                environ_prefix=None,
            ),
            "user_list_burst": values.Value(
                default="30/minute",
                environ_name="API_USERS_LIST_THROTTLE_RATE_BURST",
                environ_prefix=None,
            ),
            # /calendar/conflicts/ PROPFINDs the home set and REPORTs every
            # calendar in it; legitimate UI use is one call per opened
            # invite. 30/min/user is generous for users with many invites
            # in a thread but caps the cost of a runaway script.
            "caldav_conflicts": values.Value(
                default="30/minute",
                environ_name="API_CALDAV_CONFLICTS_THROTTLE_RATE",
                environ_prefix=None,
            ),
            # Public widget deliver endpoint. The channel id is a public embed
            # value, so these caps bound abuse: a per-channel ceiling on total
            # inbound volume and a per-IP burst limit under it.
            "widget_inbound_channel": values.Value(
                default="30/minute",
                environ_name="API_WIDGET_INBOUND_CHANNEL_THROTTLE_RATE",
                environ_prefix=None,
            ),
            "widget_inbound_ip": values.Value(
                default="10/minute",
                environ_name="API_WIDGET_INBOUND_IP_THROTTLE_RATE",
                environ_prefix=None,
            ),
        },
    }

    SPECTACULAR_SETTINGS = {
        "TITLE": "messages API",
        "DESCRIPTION": "This is the messages API schema.",
        "VERSION": "1.0.0",
        "SERVE_INCLUDE_SCHEMA": False,
        "ENABLE_DJANGO_DEPLOY_CHECK": values.BooleanValue(
            default=False,
            environ_name="SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK",
        ),
        "COMPONENT_SPLIT_REQUEST": True,
        # OTHER SETTINGS
        "SWAGGER_UI_DIST": "SIDECAR",  # shorthand to use the sidecar instead
        "SWAGGER_UI_FAVICON_HREF": "SIDECAR",
        "REDOC_DIST": "SIDECAR",
    }

    TRASHBIN_CUTOFF_DAYS = values.Value(
        30, environ_name="TRASHBIN_CUTOFF_DAYS", environ_prefix=None
    )

    AUTH_USER_MODEL = "core.User"

    # CORS
    CORS_ALLOW_CREDENTIALS = True
    CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False)
    CORS_ALLOWED_ORIGINS = values.ListValue([])
    CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])

    # Sentry
    SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None)

    # Frontend
    # These settings default to None ("not configured") on purpose: the config
    # endpoint omits unset entries so the frontend can still fall back on its
    # deprecated NEXT_PUBLIC_* build-time variables during the migration.
    # Sending backend defaults instead would silently override those variables.
    FRONTEND_SILENT_LOGIN_ENABLED = values.BooleanValue(
        default=False, environ_name="FRONTEND_SILENT_LOGIN_ENABLED", environ_prefix=None
    )
    FRONTEND_THEME_CONFIG = JSONValue(
        None, environ_name="FRONTEND_THEME_CONFIG", environ_prefix=None
    )
    FRONTEND_FORCED_DEFAULT_LANGUAGE = OptionalBooleanValue(
        None,
        environ_name="FRONTEND_FORCED_DEFAULT_LANGUAGE",
        environ_prefix=None,
    )
    FRONTEND_MULTIPART_UPLOAD_CHUNK_SIZE_MB = values.PositiveIntegerValue(
        None,
        environ_name="FRONTEND_MULTIPART_UPLOAD_CHUNK_SIZE_MB",
        environ_prefix=None,
    )
    FRONTEND_HELP_CENTER_URL = values.Value(
        None, environ_name="FRONTEND_HELP_CENTER_URL", environ_prefix=None
    )
    # Expected keys: api_url, path, channel, home_channel
    FRONTEND_FEEDBACK_WIDGET_CONFIG = JSONValue(
        None, environ_name="FRONTEND_FEEDBACK_WIDGET_CONFIG", environ_prefix=None
    )
    # Expected keys: api_url, path
    FRONTEND_LAGAUFRE_WIDGET_CONFIG = JSONValue(
        None, environ_name="FRONTEND_LAGAUFRE_WIDGET_CONFIG", environ_prefix=None
    )

    # Celery
    CELERY_BROKER_URL = values.Value(
        "redis://redis:6379", environ_name="CELERY_BROKER_URL", environ_prefix=None
    )
    CELERY_RESULT_BACKEND = "django-db"
    CELERY_CACHE_BACKEND = "django-cache"
    CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({})
    CELERY_RESULT_EXTENDED = True
    CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 * 30  # 30 days
    CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
    CELERY_WORKER_HIJACK_ROOT_LOGGER = False

    # Default queue for tasks without explicit routing
    CELERY_TASK_DEFAULT_QUEUE = "default"

    # Queue routing - tasks are routed to specific queues based on their type
    # Priority order: management > inbound > outbound > default > imports > reindex
    CELERY_TASK_ROUTES = {
        # Inbound email processing - highest priority, time-sensitive
        "core.mda.inbound_tasks.*": {"queue": "inbound"},
        # Outbound email sending - high priority
        "core.mda.outbound_tasks.*": {"queue": "outbound"},
        # Import tasks - lower priority than regular tasks
        "core.services.importer.mbox_tasks.*": {"queue": "imports"},
        "core.services.importer.eml_tasks.*": {"queue": "imports"},
        "core.services.importer.imap_tasks.*": {"queue": "imports"},
        "core.services.importer.pst_tasks.*": {"queue": "imports"},
        # Search indexing - lowest priority, can be delayed
        "core.services.search.tasks.*": {"queue": "reindex"},
    }

    DISABLE_CELERY_BEAT_SCHEDULE = values.BooleanValue(
        default=False, environ_name="DISABLE_CELERY_BEAT_SCHEDULE", environ_prefix=None
    )

    CELERY_WORKER_SEND_TASK_EVENTS = values.BooleanValue(
        True, environ_name="CELERY_WORKER_SEND_TASK_EVENTS", environ_prefix=None
    )
    CELERY_TASK_SEND_SENT_EVENT = values.BooleanValue(
        True, environ_name="CELERY_TASK_SEND_SENT_EVENT", environ_prefix=None
    )

    # Session
    SESSION_ENGINE = "django.contrib.sessions.backends.cache"
    SESSION_CACHE_ALIAS = "default"
    SESSION_COOKIE_AGE = 60 * 60 * 12

    # OIDC - Authorization Code Flow
    OIDC_CREATE_USER = values.BooleanValue(
        default=False, environ_name="OIDC_CREATE_USER", environ_prefix=None
    )
    OIDC_RP_SIGN_ALGO = values.Value(
        "RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
    )
    OIDC_RP_CLIENT_ID = values.Value(
        "st_messages", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
    )
    OIDC_RP_CLIENT_SECRET = values.Value(
        None,
        environ_name="OIDC_RP_CLIENT_SECRET",
        environ_prefix=None,
    )
    OIDC_OP_JWKS_ENDPOINT = values.Value(
        environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
    )
    OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
        environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
    )
    OIDC_OP_TOKEN_ENDPOINT = values.Value(
        None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
    )
    OIDC_OP_USER_ENDPOINT = values.Value(
        None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
    )
    OIDC_OP_LOGOUT_ENDPOINT = values.Value(
        None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None
    )
    OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
        {}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
    )
    OIDC_AUTH_REQUEST_FORWARDED_PARAMS = values.ListValue(
        ["login_hint"],
        environ_name="OIDC_AUTH_REQUEST_FORWARDED_PARAMS",
        environ_prefix=None,
    )
    OIDC_RP_SCOPES = values.Value(
        "openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
    )
    OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView"
    OIDC_CALLBACK_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationCallbackView"
    LOGIN_REDIRECT_URL = values.Value(
        None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None
    )
    LOGIN_REDIRECT_URL_FAILURE = values.Value(
        None, environ_name="LOGIN_REDIRECT_URL_FAILURE", environ_prefix=None
    )
    LOGOUT_REDIRECT_URL = values.Value(
        None, environ_name="LOGOUT_REDIRECT_URL", environ_prefix=None
    )
    OIDC_USE_NONCE = values.BooleanValue(
        default=True, environ_name="OIDC_USE_NONCE", environ_prefix=None
    )
    OIDC_REDIRECT_REQUIRE_HTTPS = values.BooleanValue(
        default=False, environ_name="OIDC_REDIRECT_REQUIRE_HTTPS", environ_prefix=None
    )
    OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue(
        default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None
    )
    OIDC_STORE_ID_TOKEN = values.BooleanValue(
        default=True, environ_name="OIDC_STORE_ID_TOKEN", environ_prefix=None
    )
    OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = values.BooleanValue(
        default=True,
        environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION",
        environ_prefix=None,
    )

    # WARNING: Enabling this setting allows multiple user accounts to share the same email
    # address. This may cause security issues and is not recommended for production use when
    # email is activated as fallback for identification (see previous setting).
    OIDC_ALLOW_DUPLICATE_EMAILS = values.BooleanValue(
        default=False,
        environ_name="OIDC_ALLOW_DUPLICATE_EMAILS",
        environ_prefix=None,
    )

    OIDC_USERINFO_ESSENTIAL_CLAIMS = values.ListValue(
        default=[], environ_name="OIDC_USERINFO_ESSENTIAL_CLAIMS", environ_prefix=None
    )
    OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue(
        default=["first_name", "last_name"],
        environ_name="OIDC_USERINFO_FULLNAME_FIELDS",
        environ_prefix=None,
    )
    # Store OIDC tokens in the session
    OIDC_STORE_ACCESS_TOKEN = values.BooleanValue(
        default=False,
        environ_name="OIDC_STORE_ACCESS_TOKEN",
        environ_prefix=None,
    )  # Store the access token in the session
    OIDC_STORE_REFRESH_TOKEN = values.BooleanValue(
        default=False,
        environ_name="OIDC_STORE_REFRESH_TOKEN",
        environ_prefix=None,
    )  # Store the encrypted refresh token in the session

    # Required for refresh token encryption
    OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
        None, environ_name="OIDC_STORE_REFRESH_TOKEN_KEY", environ_prefix=None
    )

    ALLOW_LOGOUT_GET_METHOD = values.BooleanValue(
        default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
    )

    IDENTITY_PROVIDER = values.Value(
        None, environ_name="IDENTITY_PROVIDER", environ_prefix=None
    )

    KEYCLOAK_REALM = values.Value(
        None, environ_name="KEYCLOAK_REALM", environ_prefix=None
    )
    KEYCLOAK_URL = values.Value(None, environ_name="KEYCLOAK_URL", environ_prefix=None)
    KEYCLOAK_CLIENT_ID = values.Value(
        None, environ_name="KEYCLOAK_CLIENT_ID", environ_prefix=None
    )
    KEYCLOAK_CLIENT_SECRET = values.Value(
        None,
        environ_name="KEYCLOAK_CLIENT_SECRET",
        environ_prefix=None,
    )
    KEYCLOAK_GROUP_PATH_PREFIX = values.Value(
        None,
        environ_name="KEYCLOAK_GROUP_PATH_PREFIX",
        environ_prefix=None,
    )

    ENABLE_PROMETHEUS = values.BooleanValue(
        default=False, environ_name="ENABLE_PROMETHEUS", environ_prefix=None
    )

    PROMETHEUS_API_KEY = values.Value(
        None, environ_name="PROMETHEUS_API_KEY", environ_prefix=None
    )

    # DEPRECATED: ignored since global api_key Channels landed.
    # Kept only so AppConfig.ready() can emit a deprecation warning when
    # either env var is set. Migrate to a global api_key Channel.
    METRICS_API_KEY = values.Value(
        None, environ_name="METRICS_API_KEY", environ_prefix=None
    )

    PROVISIONING_API_KEY = values.Value(
        None, environ_name="PROVISIONING_API_KEY", environ_prefix=None
    )

    METRICS_STORAGE_USED_OVERHEAD_BY_MESSAGE = values.PositiveIntegerValue(
        1024,
        environ_name="METRICS_STORAGE_USED_OVERHEAD_BY_MESSAGE",
        environ_prefix=None,
    )

    # AI
    AI_API_KEY = values.Value(None, environ_name="AI_API_KEY", environ_prefix=None)
    AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
    AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)

    # Entitlements
    ENTITLEMENTS_BACKEND = values.Value(
        "core.entitlements.backends.local.LocalEntitlementsBackend",
        environ_name="ENTITLEMENTS_BACKEND",
        environ_prefix=None,
    )
    ENTITLEMENTS_BACKEND_PARAMETERS = JSONValue(
        {}, environ_name="ENTITLEMENTS_BACKEND_PARAMETERS", environ_prefix=None
    )
    ENTITLEMENTS_CACHE_TIMEOUT = values.PositiveIntegerValue(
        300, environ_name="ENTITLEMENTS_CACHE_TIMEOUT", environ_prefix=None
    )

    # Feature flags
    FEATURE_AI_SUMMARY = values.BooleanValue(
        default=False, environ_name="FEATURE_AI_SUMMARY", environ_prefix=None
    )
    FEATURE_AI_AUTOLABELS = values.BooleanValue(
        default=False, environ_name="FEATURE_AI_AUTOLABELS", environ_prefix=None
    )
    FEATURE_MESSAGE_TEMPLATES = values.BooleanValue(
        default=True, environ_name="FEATURE_MESSAGE_TEMPLATES", environ_prefix=None
    )
    FEATURE_IMPORT_MESSAGES = values.BooleanValue(
        default=True, environ_name="FEATURE_IMPORT_MESSAGES", environ_prefix=None
    )
    FEATURE_MAILBOX_ADMIN_CHANNELS = values.ListValue(
        default=["api_key", "webhook"],
        environ_name="FEATURE_MAILBOX_ADMIN_CHANNELS",
        environ_prefix=None,
    )
    FEATURE_MAILDOMAIN_CREATE = values.BooleanValue(
        default=True, environ_name="FEATURE_MAILDOMAIN_CREATE", environ_prefix=None
    )
    FEATURE_MAILDOMAIN_MANAGE_ACCESSES = values.BooleanValue(
        default=True,
        environ_name="FEATURE_MAILDOMAIN_MANAGE_ACCESSES",
        environ_prefix=None,
    )
    # Kill-switch for the "split thread" feature. When False, the
    # corresponding API action is disabled and the frontend hides the
    # related menu entry.
    FEATURE_THREAD_SPLIT = values.BooleanValue(
        default=True, environ_name="FEATURE_THREAD_SPLIT", environ_prefix=None
    )

    # Kill-switch for the mailbox Statistics settings tab (and its stats
    # endpoints in the UI). Instance-wide; not overridable per domain.
    FEATURE_MAILBOX_STATS = values.BooleanValue(
        default=True, environ_name="FEATURE_MAILBOX_STATS", environ_prefix=None
    )

    # Mandatory TOTP admin column. When True, the mailbox admin DataGrid shows
    # a "Mandatory 2FA" column with a per-mailbox switch and a "Reset 2FA"
    # row action. Both rely on KEYCLOAK_TOTP_ROLE_ID.
    FEATURE_MAILDOMAIN_MANAGE_TOTP = values.BooleanValue(
        default=False,
        environ_name="FEATURE_MAILDOMAIN_MANAGE_TOTP",
        environ_prefix=None,
    )
    # ID of the realm role assigned in Keycloak when "Mandatory 2FA" is
    # toggled on for a mailbox.
    KEYCLOAK_TOTP_ROLE_ID = values.Value(
        default=None, environ_name="KEYCLOAK_TOTP_ROLE_ID", environ_prefix=None
    )

    # Logging
    # We want to make it easy to log to console but by default we log production
    # to Sentry and don't want to log to console.
    LOGGING = {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "simple": {
                "format": "{asctime} {name} {levelname} {message}",
                "style": "{",
            },
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "formatter": "simple",
            },
        },
        # Override root logger to send it to console
        "root": {
            "handlers": ["console"],
            "level": values.Value(
                "INFO", environ_name="LOGGING_LEVEL_LOGGERS_ROOT", environ_prefix=None
            ),
        },
        "loggers": {
            "core": {
                "handlers": ["console"],
                "level": values.Value(
                    "INFO",
                    environ_name="LOGGING_LEVEL_LOGGERS_APP",
                    environ_prefix=None,
                ),
                "propagate": False,
            },
            "botocore": {
                "handlers": ["console"],
                "level": "WARNING",
                "propagate": False,
            },
            "urllib3": {
                "handlers": ["console"],
                "level": "WARNING",
                "propagate": False,
            },
            "opensearch": {
                "handlers": ["console"],
                "level": "WARNING",
                "propagate": False,
            },
            "opensearchpy": {
                "handlers": ["console"],
                "level": "WARNING",
                "propagate": False,
            },
            "opensearchpy.trace": {
                "handlers": ["console"],
                "level": "WARNING",
                "propagate": False,
            },
            "elastic_transport": {
                "handlers": ["console"],
                "level": "WARNING",
                "propagate": False,
            },
        },
    }

    # External services
    # Settings related to the third-party services that messages is able to use
    # 1. Drive - https://github.com/suitenumerique/drive
    DRIVE_CONFIG = {
        "app_name": values.Value(
            default="Drive", environ_name="DRIVE_APP_NAME", environ_prefix=None
        ),
        "base_url": values.Value(
            default=None, environ_name="DRIVE_BASE_URL", environ_prefix=None
        ),
        "file_url": "/explorer/items/files",
        "sdk_url": "/sdk",
        "api_url": "/api/v1.0",
        "preview_url": "/media/preview/item",
    }

    # pylint: disable=invalid-name
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Ensure Django's upload limit accommodates the larger of the email size limits
        # Body and attachments are uploaded separately (body as JSON, attachments as blobs),
        # so we take the maximum of individual limits, not their sum.
        # Apply 1.4x factor only for incoming emails (which arrive MIME-encoded with ~33% overhead).
        # Outgoing attachments are uploaded as raw binary files, so no encoding overhead.
        self.DATA_UPLOAD_MAX_MEMORY_SIZE = max(
            self.DATA_UPLOAD_MAX_MEMORY_SIZE,
            int(self.MAX_INCOMING_EMAIL_SIZE * 1.4),  # MIME encoding overhead
            self.MAX_OUTGOING_BODY_SIZE,  # Raw JSON, minimal overhead
            self.MAX_OUTGOING_ATTACHMENT_SIZE,  # Raw binary upload, no encoding
        )

        if self.ENABLE_PROMETHEUS:
            self.INSTALLED_APPS += ["django_prometheus"]
            self.MIDDLEWARE = [
                "core.middlewares.PrometheusAuthMiddleware",
                "django_prometheus.middleware.PrometheusBeforeMiddleware",
                *self.MIDDLEWARE,
                "django_prometheus.middleware.PrometheusAfterMiddleware",
            ]

        if self.USE_X_FORWARDED_FOR:
            self.MIDDLEWARE.insert(0, "core.middlewares.XForwardedForMiddleware")

        if os.environ.get("MTA_OUT_SMTP_HOST") and not self.MTA_OUT_RELAY_HOST:
            logger.warning(
                "[DEPRECATED] MTA_OUT_SMTP_HOST is deprecated, use MTA_OUT_RELAY_HOST instead"
            )
            self.MTA_OUT_RELAY_HOST = os.environ.get("MTA_OUT_SMTP_HOST")
            self.MTA_OUT_RELAY_USERNAME = os.environ.get("MTA_OUT_SMTP_USERNAME")
            self.MTA_OUT_RELAY_PASSWORD = os.environ.get("MTA_OUT_SMTP_PASSWORD")

        if self.MTA_OUT_SMTP_TLS_SECURITY_LEVEL not in {"none", "may", "secure"}:
            raise ValueError(
                f"Invalid MTA_OUT_SMTP_TLS_SECURITY_LEVEL: {self.MTA_OUT_SMTP_TLS_SECURITY_LEVEL}"
            )

        # Deprecated fields mapping
        deprecated_settings_mapping = (
            (
                "USER_OIDC_ESSENTIAL_CLAIMS",
                "OIDC_USERINFO_ESSENTIAL_CLAIMS",
                values.ListValue(),
            ),
            (
                "USER_OIDC_FIELDS_TO_FULLNAME",
                "OIDC_USERINFO_FULLNAME_FIELDS",
                values.ListValue(),
            ),
            ("USER_OIDC_FIELD_TO_SHORTNAME", None, None),
            # FRONTEND_THEME was never honored; FRONTEND_THEME_CONFIG is the
            # intentional replacement, so the old value is not mapped over.
            ("FRONTEND_THEME", None, None),
        )

        for deprecated_setting, new_setting, Parser in deprecated_settings_mapping:
            if value := os.environ.get(deprecated_setting):
                if new_setting is None:
                    logger.warning(
                        (
                            "[DEPRECATED] %s is deprecated, "
                            "it is unused and will be removed in the future"
                        ),
                        deprecated_setting,
                    )
                    continue
                if os.environ.get(new_setting) is None:
                    setattr(self, new_setting, Parser.to_python(value=value))
                logger.warning(
                    "[DEPRECATED] %s is deprecated, use %s instead",
                    deprecated_setting,
                    new_setting,
                )

    # pylint: disable=invalid-name
    @property
    def ENVIRONMENT(self):
        """Environment in which the application is launched."""
        return self.__class__.__name__.lower()

    # pylint: disable=invalid-name
    @property
    def RELEASE(self):
        """
        Return the release information.

        Delegate to the module function to enable easier testing.
        """
        return get_release()

    @classmethod
    def post_setup(cls):
        """Post setup configuration.
        This is the place where you can configure settings that require other
        settings to be loaded.
        """
        super().post_setup()

        # The SENTRY_DSN setting should be available to activate sentry for an environment
        if cls.SENTRY_DSN is not None:
            sentry_sdk.init(
                dsn=cls.SENTRY_DSN,
                environment=cls.__name__.lower(),
                release=get_release(),
                integrations=[DjangoIntegration()],
            )
            sentry_sdk.set_tag("application", "backend")

        if (
            cls.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
            and cls.OIDC_ALLOW_DUPLICATE_EMAILS
        ):
            raise ValueError(
                "Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
                "OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
            )

        if cls.OIDC_STORE_REFRESH_TOKEN and not cls.OIDC_STORE_REFRESH_TOKEN_KEY:
            raise ValueError(
                "OIDC_STORE_REFRESH_TOKEN_KEY must be set when "
                "OIDC_STORE_REFRESH_TOKEN is enabled."
            )


class Build(Base):
    """Settings used when the application is built.

    This environment should not be used to run the application. Just to build it with non-blocking
    settings.
    """

    SECRET_KEY = values.Value("DummyKey")
    STORAGES = {
        "default": {
            "BACKEND": "django.core.files.storage.FileSystemStorage",
        },
        "staticfiles": {
            "BACKEND": values.Value(
                "whitenoise.storage.CompressedManifestStaticFilesStorage",
                environ_name="STORAGES_STATICFILES_BACKEND",
            ),
        },
    }


class Development(Base):
    """
    Development environment settings

    We set DEBUG to True and configure the server to respond from all hosts.
    """

    ALLOWED_HOSTS = ["*"]
    CORS_ALLOW_ALL_ORIGINS = True
    CSRF_TRUSTED_ORIGINS = ["http://localhost:8900", "http://localhost:8901"]
    DEBUG = True

    SESSION_COOKIE_NAME = "st_messages_sessionid"

    ENABLE_PROMETHEUS = True
    PROMETHEUS_API_KEY = "local_api_key"

    USE_SWAGGER = True
    SESSION_CACHE_ALIAS = "session"
    # Use Redis for caching (required for task owner tracking)
    CACHES = {
        "default": {
            "BACKEND": "django_redis.cache.RedisCache",
            "LOCATION": values.Value(
                "redis://redis:6379",
                environ_name="REDIS_URL",
                environ_prefix=None,
            ),
            "TIMEOUT": values.IntegerValue(
                30,  # timeout in seconds
                environ_name="CACHES_DEFAULT_TIMEOUT",
                environ_prefix=None,
            ),
            "OPTIONS": {
                "CLIENT_CLASS": "django_redis.client.DefaultClient",
            },
        },
        "session": {
            "BACKEND": "django_redis.cache.RedisCache",
            "LOCATION": values.Value(
                "redis://redis:6379",
                environ_name="REDIS_URL",
                environ_prefix=None,
            ),
            "TIMEOUT": values.IntegerValue(
                30,  # timeout in seconds
                environ_name="CACHES_DEFAULT_TIMEOUT",
                environ_prefix=None,
            ),
            "OPTIONS": {
                "CLIENT_CLASS": "django_redis.client.DefaultClient",
            },
        },
    }

    # pylint: disable=invalid-name
    def __init__(self):
        super().__init__()
        self.INSTALLED_APPS += ["django_extensions", "drf_spectacular_sidecar"]


class E2E(Development):
    """
    End2End environment settings

    Uses Caddy reverse proxy to serve both frontend and backend on the same origin,
    avoiding cross-origin cookie issues.
    """

    # Include the e2e app only in E2E environment
    INSTALLED_APPS = Development.INSTALLED_APPS + ["e2e"]

    CSRF_TRUSTED_ORIGINS = ["http://proxy", "http://keycloak:8802"]

    # Trust X-Forwarded-* headers from Caddy proxy
    USE_X_FORWARDED_HOST = True


class DevelopmentMinimal(Development):
    """
    Development environment settings with minimal dependencies
    """

    CELERY_TASK_ALWAYS_EAGER = True
    OPENSEARCH_INDEX_THREADS = False
    # LocMemCache (not DummyCache) for the default cache so that features
    # that depend on real caching — notably task-owner tracking used by
    # async-task polling — work in this no-Redis profile.
    CACHES = {
        "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
        "session": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
    }


class Test(Base):
    """Test environment settings"""

    PASSWORD_HASHERS = [
        "django.contrib.auth.hashers.MD5PasswordHasher",
    ]
    USE_SWAGGER = True

    IDENTITY_PROVIDER = None

    CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True)

    AWS_S3_DOMAIN_REPLACE = None

    # Add a test encryption key for django-fernet-encrypted-fields
    SALT_KEY = ["test-salt-for-development-only"]

    FEATURE_MAILBOX_ADMIN_CHANNELS = ["api_key", "widget", "webhook"]

    # Tests must not depend on a reachable rspamd. With no rspamd_url the
    # spam step is a no-op ("no opinion"), so inbound delivery tests
    # deliver deterministically; spam-specific tests opt in explicitly via
    # ``@override_settings(SPAM_CONFIG={"rspamd_url": ...})`` and mock the
    # HTTP call. (A configured-but-unreachable rspamd now *holds* mail for
    # retry instead of failing open — see ``_make_rspamd_step`` — so an
    # inherited env SPAM_CONFIG would otherwise stall delivery in tests.)
    SPAM_CONFIG = {}

    SCHEMA_CUSTOM_ATTRIBUTES_USER = {}
    SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN = {}

    MESSAGES_BLOBS_OFFLOAD_ENABLED = True

    # ``core.E005`` requires CACHES['default'] to use django_redis when
    # offload is enabled. Tests that exercise Redis-backed primitives
    # opt in via the ``redis_cache`` fixture (see core/tests/conftest.py),
    # which swaps CACHES per-test; the boot-time check would block
    # ``manage.py spectacular`` (and any non-Redis test invocation) for
    # no real reason in the test environment.
    SILENCED_SYSTEM_CHECKS = ["core.E005"]

    ENABLE_PROMETHEUS = True
    PROMETHEUS_API_KEY = "test_api_key"

    DRIVE_CONFIG = {
        "app_name": "Drive",
        "base_url": "http://drive.test",
        "file_url": "/explorer/items/files",
        "sdk_url": "/sdk",
        "api_url": "/api/v1.0",
        "preview_url": "/media/preview/item",
    }

    # pylint: disable=invalid-name
    def __init__(self):
        super().__init__()
        self.INSTALLED_APPS += ["drf_spectacular_sidecar"]


class ContinuousIntegration(Test):
    """
    Continuous Integration environment settings

    nota bene: it should inherit from the Test environment.
    """


class Production(Base):
    """
    Production environment settings

    You must define the ALLOWED_HOSTS environment variable in Production
    configuration (and derived configurations):
    ALLOWED_HOSTS=["foo.com", "foo.fr"]
    """

    # Security
    # Add allowed host from environment variables.
    # The machine hostname is added by default,
    # it makes the application pingable by a load balancer on the same machine by example
    ALLOWED_HOSTS = [
        *values.ListValue([], environ_name="ALLOWED_HOSTS"),
        gethostbyname(gethostname()),
    ]
    CSRF_TRUSTED_ORIGINS = values.ListValue([])
    SECURE_BROWSER_XSS_FILTER = True
    SECURE_CONTENT_TYPE_NOSNIFF = True

    # SECURE_PROXY_SSL_HEADER allows to fix the scheme in Django's HttpRequest
    # object when your application is behind a reverse proxy.
    #
    # Keep this SECURE_PROXY_SSL_HEADER configuration only if :
    # - your Django app is behind a proxy.
    # - your proxy strips the X-Forwarded-Proto header from all incoming requests
    # - Your proxy sets the X-Forwarded-Proto header and sends it to Django
    #
    # In other cases, you should comment the following line to avoid security issues.
    # SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
    SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
    SECURE_HSTS_SECONDS = 60
    SECURE_HSTS_PRELOAD = True
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SECURE_SSL_REDIRECT = True
    SECURE_REDIRECT_EXEMPT = [
        "^__lbheartbeat__",
        "^__heartbeat__",
        "^api/v1\\.0/mta/",
        "^api/v1\\.0/inbound/mta/",
    ]

    # Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
    CSRF_COOKIE_SECURE = True
    SESSION_COOKIE_SECURE = True

    # Privacy
    SECURE_REFERRER_POLICY = "same-origin"

    CACHES = {
        "default": {
            "BACKEND": "django_redis.cache.RedisCache",
            "LOCATION": values.Value(
                "redis://redis:6379",
                environ_name="REDIS_URL",
                environ_prefix=None,
            ),
            "TIMEOUT": values.IntegerValue(
                30,  # timeout in seconds
                environ_name="CACHES_DEFAULT_TIMEOUT",
                environ_prefix=None,
            ),
            "OPTIONS": {
                "CLIENT_CLASS": "django_redis.client.DefaultClient",
            },
        },
    }


class Feature(Production):
    """
    Feature environment settings

    nota bene: it should inherit from the Production environment.
    """


class Staging(Production):
    """
    Staging environment settings

    nota bene: it should inherit from the Production environment.
    """


class PreProduction(Production):
    """
    Pre-production environment settings

    nota bene: it should inherit from the Production environment.
    """
