"""
Django settings for config project.

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

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

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""

import contextlib
import importlib.util
import ipaddress
import os
import socket
from pathlib import Path
from apps.loggers import build_logging_settings
from django.utils.translation import gettext_lazy as _
from datetime import timedelta

from django.apps import AppConfig as DjangoAppConfig
from celery.schedules import crontab
from django.http import HttpRequest, request as http_request
from django.http.request import split_domain_port
from django.middleware.csrf import CsrfViewMiddleware
from django.core.exceptions import DisallowedHost, ImproperlyConfigured
from django.contrib.sites import shortcuts as sites_shortcuts
from django.contrib.sites.requests import RequestSite
from urllib.parse import urlsplit
import django.utils.encoding as encoding
from apps.celery.utils import resolve_celery_shutdown_timeout
from config.whitenoise import add_headers as whitenoise_add_headers
from config.request_utils import is_https_request
from utils.env import env_bool

from config.settings_helpers import (
    discover_local_ip_addresses,
    extract_ip_from_host,
    install_validate_host_with_subnets,
    load_secret_key,
    strip_ipv6_brackets,
)

if not hasattr(encoding, "force_text"):  # pragma: no cover - Django>=5 compatibility
    from django.utils.encoding import force_str

    encoding.force_text = force_str
install_validate_host_with_subnets()

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
APPS_DIR = BASE_DIR / "apps"

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = load_secret_key(BASE_DIR)

# SECURITY WARNING: don't run with debug turned on in production!

# Determine the current node role for role-specific settings while leaving
# DEBUG control to the environment.
NODE_ROLE = os.environ.get("NODE_ROLE")
if NODE_ROLE is None:
    role_lock = BASE_DIR / ".locks" / "role.lck"
    NODE_ROLE = role_lock.read_text().strip() if role_lock.exists() else "Terminal"

PRODUCTION_ROLES = {
    "watchtower",
    "constellation",
    "satellite",
    "control",
    "terminal",
    "gateway",
}

_debugpy_attached = "DEBUGPY_LAUNCHER_PORT" in os.environ
DEBUG = env_bool("DEBUG", _debugpy_attached)
HAS_DEBUG_TOOLBAR = DEBUG and importlib.util.find_spec("debug_toolbar") is not None


def _dedupe_app_labels(app_paths: list[str]) -> list[str]:
    """Return a list of app paths with duplicate labels removed."""

    deduped: list[str] = []
    seen_labels: set[str] = set()
    for entry in app_paths:
        label = DjangoAppConfig.create(entry).label
        if label in seen_labels:
            continue

        seen_labels.add(label)
        deduped.append(entry)

    return deduped

# Disable NetMessage propagation when running maintenance commands that should
# avoid contacting remote peers.
NET_MESSAGE_DISABLE_PROPAGATION = env_bool(
    "NET_MESSAGE_DISABLE_PROPAGATION", False
)

ENABLE_USAGE_ANALYTICS = env_bool("ENABLE_USAGE_ANALYTICS", False)

ALLOWED_HOSTS = [
    "localhost",
    "127.0.0.1",
    "testserver",
    "10.42.0.0/16",
    "192.168.0.0/16",
    "arthexis.com",
    "www.arthexis.com",
    "m.arthexis.com",
    ".arthexis.com",
]

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True


_DEFAULT_PORTS = {"http": "80", "https": "443"}


def _get_allowed_hosts() -> list[str]:
    from django.conf import settings as django_settings

    configured = getattr(django_settings, "ALLOWED_HOSTS", None)
    if configured is None:
        return ALLOWED_HOSTS
    return list(configured)


def _iter_local_hostnames(hostname: str, fqdn: str | None = None) -> list[str]:
    """Return unique hostname variants for the current machine."""

    hostnames: list[str] = []
    seen: set[str] = set()

    def _append(candidate: str | None) -> None:
        if not candidate:
            return
        normalized = candidate.strip()
        if not normalized or normalized in seen:
            return
        hostnames.append(normalized)
        seen.add(normalized)

    _append(hostname)
    _append(fqdn)
    if hostname and "." not in hostname:
        _append(f"{hostname}.local")

    return hostnames


_local_hostname = socket.gethostname().strip()
_local_fqdn = ""
with contextlib.suppress(Exception):
    _local_fqdn = socket.getfqdn().strip()

for host in _iter_local_hostnames(_local_hostname, _local_fqdn):
    if host not in ALLOWED_HOSTS:
        ALLOWED_HOSTS.append(host)


for address in discover_local_ip_addresses():
    if address not in ALLOWED_HOSTS:
        ALLOWED_HOSTS.append(address)


CACHE_LOCATION = os.environ.get("DJANGO_CACHE_DIR", str(BASE_DIR / "cache"))
with contextlib.suppress(OSError):
    os.makedirs(CACHE_LOCATION, exist_ok=True)

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": CACHE_LOCATION,
        "TIMEOUT": None,
    }
}


# Allow CSRF origin verification for hosts within allowed subnets.
_original_origin_verified = CsrfViewMiddleware._origin_verified
_original_check_referer = CsrfViewMiddleware._check_referer


def _host_is_allowed(host: str, allowed_hosts: list[str]) -> bool:
    if http_request.validate_host(host, allowed_hosts):
        return True
    domain, _port = split_domain_port(host)
    if domain and domain != host:
        return http_request.validate_host(domain, allowed_hosts)
    return False


def _parse_forwarded_header(header_value: str) -> list[dict[str, str]]:
    entries: list[dict[str, str]] = []
    if not header_value:
        return entries
    for forwarded_part in header_value.split(","):
        entry: dict[str, str] = {}
        for element in forwarded_part.split(";"):
            if "=" not in element:
                continue
            key, value = element.split("=", 1)
            entry[key.strip().lower()] = value.strip().strip('"')
        if entry:
            entries.append(entry)
    return entries


def _get_request_scheme(request, forwarded_entry: dict[str, str] | None = None) -> str:
    """Return the scheme used by the client, honoring proxy headers."""

    if forwarded_entry and forwarded_entry.get("proto", "").lower() in {"http", "https"}:
        return forwarded_entry["proto"].lower()

    if is_https_request(request):
        return "https"

    forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO", "")
    if forwarded_proto:
        candidate = forwarded_proto.split(",")[0].strip().lower()
        if candidate in {"http", "https"}:
            return candidate

    forwarded_header = request.META.get("HTTP_FORWARDED", "")
    for forwarded_entry in _parse_forwarded_header(forwarded_header):
        candidate = forwarded_entry.get("proto", "").lower()
        if candidate in {"http", "https"}:
            return candidate

    return "http"


def _normalize_origin_tuple(scheme: str | None, host: str) -> tuple[str, str, str | None] | None:
    if not scheme or scheme.lower() not in {"http", "https"}:
        return None
    domain, port = split_domain_port(host)
    normalized_host = strip_ipv6_brackets(domain.strip().lower())
    if not normalized_host:
        return None
    normalized_port = port.strip() if isinstance(port, str) else port
    if not normalized_port:
        normalized_port = _DEFAULT_PORTS.get(scheme.lower())
    if normalized_port is not None:
        normalized_port = str(normalized_port)
    return scheme.lower(), normalized_host, normalized_port


def _normalized_request_origin(origin: str) -> tuple[str, str, str | None] | None:
    parsed = urlsplit(origin)
    if not parsed.scheme or not parsed.hostname:
        return None
    scheme = parsed.scheme.lower()
    host = parsed.hostname.lower()
    port = str(parsed.port) if parsed.port is not None else _DEFAULT_PORTS.get(scheme)
    return scheme, host, port


def _iter_candidate_hosts(request, default_scheme: str) -> list[tuple[str | None, str]]:
    candidates: list[tuple[str | None, str]] = []

    forwarded_header = request.META.get("HTTP_FORWARDED", "")
    for forwarded_entry in _parse_forwarded_header(forwarded_header):
        host = forwarded_entry.get("host", "").strip()
        scheme = _get_request_scheme(request, forwarded_entry)
        candidates.append((scheme, host))

    forwarded_host = request.META.get("HTTP_X_FORWARDED_HOST", "")
    if forwarded_host:
        host = forwarded_host.split(",")[0].strip()
        candidates.append((default_scheme, host))

    try:
        good_host = request.get_host()
    except DisallowedHost:
        good_host = ""
    if good_host:
        candidates.append((default_scheme, good_host))

    return candidates


def _normalize_candidate(
    scheme: str | None,
    host: str,
    allowed_hosts: list[str],
    seen: set[tuple[str, str, str | None]],
) -> tuple[str, str, str | None] | None:
    if not scheme or not host:
        return None
    normalized = _normalize_origin_tuple(scheme, host)
    if normalized is None:
        return None
    if not _host_is_allowed(host, allowed_hosts):
        return None
    if normalized in seen:
        return None
    seen.add(normalized)
    return normalized


def _candidate_origin_tuples(
    request, allowed_hosts: list[str]
) -> list[tuple[str, str, str | None]]:
    default_scheme = _get_request_scheme(request)
    seen: set[tuple[str, str, str | None]] = set()
    candidates: list[tuple[str, str, str | None]] = []

    for scheme, host in _iter_candidate_hosts(request, default_scheme):
        normalized = _normalize_candidate(scheme, host, allowed_hosts, seen)
        if normalized is not None:
            candidates.append(normalized)

    return candidates


def _hosts_share_allowed_subnet(
    first_host: str, second_host: str, allowed_hosts: list[str]
) -> bool:
    first_ip = extract_ip_from_host(first_host)
    second_ip = extract_ip_from_host(second_host)
    if not first_ip or not second_ip:
        return False
    for pattern in allowed_hosts:
        try:
            network = ipaddress.ip_network(pattern)
        except ValueError:
            continue
        if first_ip in network and second_ip in network:
            return True
    return False


def _origin_in_candidates(
    origin: tuple[str, str, str | None],
    request,
    allowed_hosts: list[str],
) -> bool:
    for candidate in _candidate_origin_tuples(request, allowed_hosts):
        if candidate == origin:
            return True
        if _hosts_share_allowed_subnet(candidate[1], origin[1], allowed_hosts):
            return True
    return False


def _origin_verified_with_subnets(self, request):
    request_origin = request.META["HTTP_ORIGIN"]
    allowed_hosts = _get_allowed_hosts()
    normalized_origin = _normalized_request_origin(request_origin)
    if normalized_origin is None:
        return _original_origin_verified(self, request)

    if _origin_in_candidates(normalized_origin, request, allowed_hosts):
        return True
    return _original_origin_verified(self, request)


CsrfViewMiddleware._origin_verified = _origin_verified_with_subnets


def _check_referer_with_forwarded(self, request):
    referer = request.META.get("HTTP_REFERER")
    if referer is None:
        return _original_check_referer(self, request)

    try:
        parsed = urlsplit(referer)
    except ValueError:
        return _original_check_referer(self, request)

    if "" in (parsed.scheme, parsed.netloc):
        return _original_check_referer(self, request)

    if parsed.scheme.lower() != "https":
        return _original_check_referer(self, request)

    normalized_referer = _normalize_origin_tuple(parsed.scheme.lower(), parsed.netloc)
    if normalized_referer is None:
        return _original_check_referer(self, request)

    allowed_hosts = _get_allowed_hosts()

    if _origin_in_candidates(normalized_referer, request, allowed_hosts):
        return

    return _original_check_referer(self, request)


CsrfViewMiddleware._check_referer = _check_referer_with_forwarded


# Application definition

LOCAL_APPS = [
    "apps.base",
    "apps.credentials",
    "apps.celery",
    "apps.nodes",
    "apps.discovery",
    "apps.ftp",
    "apps.dns",
    "apps.screens",
    "apps.sensors",
    "apps.pyxel",
    "apps.counters",
    "apps.energy",
    "apps.groups",
    "apps.core",
    "apps.users",
    "apps.leads",
    "apps.embeds",
    "apps.flows",
    "apps.release",
    "apps.emails",
    "apps.payments",
    "apps.sponsors",
    "apps.links",
    "apps.docs",
    "apps.maps",
    "apps.locals",
    "apps.locale",
    "apps.content",
    "apps.clocks",
    "apps.audio",
    "apps.video",
    "apps.media",
    "apps.mermaid",
    "apps.odoo",
    "apps.sigils",
    "apps.selenium",
    "apps.repos",
    "apps.reports",
    "apps.app",
    "apps.rates",
    "apps.vehicle",
    "apps.protocols",
    "apps.ocpp",
    "apps.ocpp.forwarder",
    "apps.ocpp.simulator",
    "apps.meta",
    "apps.awg",
    "apps.chats",
    "apps.aws",
    "apps.socials",
    "apps.survey",
    "apps.modules",
    "apps.widgets",
    "apps.sites",
    "apps.summary",
    "apps.certs",
    "apps.nginx",
    "apps.cards",
    "apps.tasks",
    "apps.recipes",
    "apps.tests",
    "apps.teams",
    "apps.logbook",
    "apps.nmcli",
    "apps.wikis",
    "apps.totp",
    "apps.terms",
]

INSTALLED_APPS = [
    "whitenoise.runserver_nostatic",
    "django.contrib.admin",
    "django.contrib.admindocs",
    "config.auth_app.AuthConfig",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django_otp",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django_mermaid.apps.MermaidConfig",
    "parler",
    "import_export",
    "django_object_actions",
    "django.contrib.sites",
    "channels",
    "apps.celery.beat_app.CeleryBeatConfig",
] + LOCAL_APPS

INSTALLED_APPS = _dedupe_app_labels(INSTALLED_APPS)

if HAS_DEBUG_TOOLBAR:
    INSTALLED_APPS += ["debug_toolbar"]

SITE_ID = 1

MIGRATION_MODULES = {
    "sites": "apps.core.sites_migrations",
    # Pin django_celery_beat migrations to a local copy so we can override
    # upstream changes that introduce optional dependencies (e.g. Google
    # Calendar profile) and avoid InvalidBases errors during migrate.
    "django_celery_beat": "apps.celery.beat_migrations",
}

_original_get_current_site = sites_shortcuts.get_current_site


def _get_current_site_with_request_fallback(request=None):
    try:
        return _original_get_current_site(request)
    except Exception as exc:
        from django.contrib.sites.models import Site
        from django.db.utils import OperationalError, ProgrammingError

        recoverable_exceptions = (Site.DoesNotExist, OperationalError, ProgrammingError)

        if request is not None and isinstance(exc, recoverable_exceptions):
            return RequestSite(request)
        raise


sites_shortcuts.get_current_site = _get_current_site_with_request_fallback

MIDDLEWARE = [
    # Must be first to run last in the response phase to strip COOP headers.
    "config.middleware.CrossOriginOpenerPolicyMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "config.middleware.ActiveAppMiddleware",
    "config.middleware.SiteHttpsRedirectMiddleware",
    "config.middleware.ContentSecurityPolicyMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "apps.sites.middleware.LanguagePreferenceMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "config.middleware.UsageAnalyticsMiddleware",
    "apps.sigils.middleware.SigilContextMiddleware",
    "apps.sites.middleware.ViewHistoryMiddleware",
    "config.middleware.PageMissLoggingMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ANALYTICS_EXCLUDED_URL_PREFIXES = ("/__debug__", "/healthz", "/status")

if HAS_DEBUG_TOOLBAR:
    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
    INTERNAL_IPS = ["127.0.0.1", "localhost", "0.0.0.0"]

    def _show_toolbar(_: HttpRequest) -> bool:
        """Always show the toolbar when running in DEBUG mode.

        Docker and remote development environments can present requests from
        gateway addresses that fall outside ``INTERNAL_IPS``. Returning
        ``True`` here ensures the toolbar is available whenever the debug
        flag is enabled.
        """

        return True

    DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": _show_toolbar}

CSRF_FAILURE_VIEW = "apps.sites.views.csrf_failure"

# Allow staff TODO pages to embed internal admin views inside iframes.
X_FRAME_OPTIONS = "SAMEORIGIN"

ROOT_URLCONF = "config.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "apps" / "sites" / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.template.context_processors.i18n",
                "django.contrib.messages.context_processors.messages",
                "config.context_processors.site_and_node",
                "apps.sites.context_processors.nav_links",
                "apps.links.context_processors.share_short_url",
            ],
        },
    },
]

WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"

# Channels configuration
CHANNEL_REDIS_URL = os.environ.get("CHANNEL_REDIS_URL", "").strip()
OCPP_STATE_REDIS_URL = os.environ.get("OCPP_STATE_REDIS_URL", "").strip()
if not OCPP_STATE_REDIS_URL:
    OCPP_STATE_REDIS_URL = CHANNEL_REDIS_URL or os.environ.get("CELERY_BROKER_URL", "").strip()

VIDEO_FRAME_REDIS_URL = os.environ.get("VIDEO_FRAME_REDIS_URL", "").strip()
VIDEO_FRAME_CACHE_PREFIX = os.environ.get("VIDEO_FRAME_CACHE_PREFIX", "video:mjpeg")
VIDEO_FRAME_CACHE_TTL = int(os.environ.get("VIDEO_FRAME_CACHE_TTL", "10") or 10)
VIDEO_FRAME_MAX_AGE_SECONDS = int(
    os.environ.get("VIDEO_FRAME_MAX_AGE_SECONDS", "15") or 15
)
VIDEO_FRAME_CAPTURE_INTERVAL = float(
    os.environ.get("VIDEO_FRAME_CAPTURE_INTERVAL", "0.2") or 0.2
)
VIDEO_FRAME_POLL_INTERVAL = float(
    os.environ.get("VIDEO_FRAME_POLL_INTERVAL", "0.2") or 0.2
)
VIDEO_FRAME_SERVICE_SLEEP = float(
    os.environ.get("VIDEO_FRAME_SERVICE_SLEEP", "0.05") or 0.05
)

if CHANNEL_REDIS_URL or OCPP_STATE_REDIS_URL:
    from channels_redis.core import RedisChannelLayer

    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels_redis.core.RedisChannelLayer",
            "CONFIG": {
                "hosts": [CHANNEL_REDIS_URL or OCPP_STATE_REDIS_URL],
            },
        }
    }
else:
    CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}

OCPP_PENDING_CALL_TTL = int(os.environ.get("OCPP_PENDING_CALL_TTL", "1800") or 1800)
OCPP_ASYNC_LOGGING = env_bool(
    "OCPP_ASYNC_LOGGING", bool(CHANNEL_REDIS_URL or OCPP_STATE_REDIS_URL)
)

PAGES_CHAT_ENABLED = env_bool("PAGES_CHAT_ENABLED", True)
PAGES_CHAT_NOTIFY_STAFF = env_bool("PAGES_CHAT_NOTIFY_STAFF", False)
try:
    PAGES_CHAT_IDLE_ESCALATE_SECONDS = int(
        os.environ.get("PAGES_CHAT_IDLE_ESCALATE_SECONDS", "300")
    )
except (TypeError, ValueError):
    PAGES_CHAT_IDLE_ESCALATE_SECONDS = 300
PAGES_CHAT_SOCKET_PATH = os.environ.get("PAGES_CHAT_SOCKET_PATH", "/ws/pages/chat/")


# Custom user model
AUTH_USER_MODEL = "users.User"

# Enable RFID authentication backend and restrict default admin login to localhost
# Keep LocalhostAdminBackend first so the localhost/IP checks run before password
# or OTP authentication.
AUTHENTICATION_BACKENDS = [
    "apps.users.backends.LocalhostAdminBackend",
    "apps.users.backends.PasswordOrOTPBackend",
    "apps.users.backends.TempPasswordBackend",
    "apps.users.backends.RFIDBackend",
]

# Use the custom login view for all authentication redirects.
LOGIN_URL = "pages:login"

# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases

FORCED_DB_BACKEND = os.environ.get("ARTHEXIS_DB_BACKEND", "").strip().lower()
if FORCED_DB_BACKEND and FORCED_DB_BACKEND not in {"sqlite", "postgres"}:
    raise ImproperlyConfigured(
        "ARTHEXIS_DB_BACKEND must be 'sqlite' or 'postgres' when defined."
    )


def _postgres_available() -> bool:
    if FORCED_DB_BACKEND == "sqlite":
        return False
    try:
        import psycopg
    except Exception:
        return False

    params = {
        "dbname": os.environ.get("POSTGRES_DB", "postgres"),
        "user": os.environ.get("POSTGRES_USER", "postgres"),
        "password": os.environ.get("POSTGRES_PASSWORD", ""),
        "host": os.environ.get("POSTGRES_HOST", "localhost"),
        "port": os.environ.get("POSTGRES_PORT", "5432"),
        "connect_timeout": 10,
    }
    try:
        with contextlib.closing(psycopg.connect(**params)):
            return True
    except psycopg.OperationalError:
        return False


if FORCED_DB_BACKEND == "postgres":
    _use_postgres = True
elif FORCED_DB_BACKEND == "sqlite":
    _use_postgres = False
else:
    _use_postgres = _postgres_available()


if _use_postgres:
    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.postgresql",
            "NAME": os.environ.get("POSTGRES_DB", "postgres"),
            "USER": os.environ.get("POSTGRES_USER", "postgres"),
            "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""),
            "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
            "PORT": os.environ.get("POSTGRES_PORT", "5432"),
            "OPTIONS": {"options": "-c timezone=UTC"},
            "TEST": {
                "NAME": f"{os.environ.get('POSTGRES_DB', 'postgres')}_test",
            },
        }
    }
else:
    _sqlite_override = os.environ.get("ARTHEXIS_SQLITE_PATH")
    if _sqlite_override:
        SQLITE_DB_PATH = Path(_sqlite_override)
    else:
        SQLITE_DB_PATH = BASE_DIR / "db.sqlite3"

    _sqlite_test_override = os.environ.get("ARTHEXIS_SQLITE_TEST_PATH")
    if _sqlite_test_override:
        SQLITE_TEST_DB_PATH = Path(_sqlite_test_override)
    else:
        SQLITE_TEST_DB_PATH = BASE_DIR / "work" / "test_db" / "test_db.sqlite3"
    SQLITE_TEST_DB_PATH.parent.mkdir(parents=True, exist_ok=True)

    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.sqlite3",
            "NAME": SQLITE_DB_PATH,
            "OPTIONS": {"timeout": 60},
            "TEST": {"NAME": SQLITE_TEST_DB_PATH},
        }
    }


# Password validation
# https://docs.djangoproject.com/en/5.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",
    },
]


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

LANGUAGE_CODE = "en-us"

LANGUAGES = [
    ("es", _("Spanish (Latin America)")),
    ("en", _("English")),
    ("it", _("Italian")),
    ("de", _("German")),
]

PARLER_DEFAULT_LANGUAGE_CODE = "en"

PARLER_LANGUAGES = {
    None: (
        {"code": "en"},
        {"code": "es"},
        {"code": "it"},
        {"code": "de"},
    ),
    "default": {
        "fallbacks": ["en"],
        "hide_untranslated": False,
    },
}

LOCALE_PATHS = [BASE_DIR / "apps" / "locale" / "locale"]

FORMAT_MODULE_PATH = ["config.formats"]

TIME_ZONE = "America/Monterrey"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "static"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
WHITENOISE_ADD_HEADERS_FUNCTION = whitenoise_add_headers

# Allow development and freshly-updated environments to serve assets which have
# not yet been collected into ``STATIC_ROOT``. Without this setting WhiteNoise
# only looks for files inside ``STATIC_ROOT`` and dashboards like the public
# traffic chart fail to load their JavaScript dependencies.
WHITENOISE_USE_FINDERS = True
WHITENOISE_AUTOREFRESH = DEBUG

MERMAID_USE_CDN = True
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

# Email settings
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
DEFAULT_FROM_EMAIL = "arthexis@gmail.com"
SERVER_EMAIL = DEFAULT_FROM_EMAIL

# Slack bot onboarding
SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", "")
SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", "")
SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "")
SLACK_BOT_SCOPES = os.environ.get(
    "SLACK_BOT_SCOPES",
    "commands,chat:write,chat:write.public",
)
SLACK_REDIRECT_URL = os.environ.get("SLACK_REDIRECT_URL", "")

# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# GitHub issue reporting
GITHUB_ISSUE_REPORTING_ENABLED = True
GITHUB_ISSUE_REPORTING_COOLDOWN = 3600  # seconds

# Logging configuration
LOG_DIR, LOG_FILE_NAME, LOGGING = build_logging_settings(BASE_DIR, DEBUG)


# Celery configuration
_ENV_CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "").strip()
CELERY_BROKER_URL = _ENV_CELERY_BROKER_URL or "memory://localhost/"
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "cache+memory://")
# Keep Celery Beat schedules in memory to avoid database-backed scheduling
# (e.g., django-celery-beat), which can contend with migrations.
CELERY_BEAT_SCHEDULER = "celery.beat:Scheduler"
CELERY_TIMEZONE = TIME_ZONE
CELERY_ENABLE_UTC = True
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
# Align Celery's log formatting with Django's logging configuration so worker
# output follows the same conventions without polluting the error log.
CELERY_WORKER_LOG_FORMAT = LOGGING["formatters"]["standard"]["format"]
CELERY_WORKER_TASK_LOG_FORMAT = LOGGING["formatters"]["standard"]["format"]
# Allow Celery workers extra time to finish acknowledged jobs before SIGTERM.
CELERY_WORKER_SOFT_SHUTDOWN_TIMEOUT = resolve_celery_shutdown_timeout()
# Legacy alias retained for fixture references and admin guidance.
CELERY_WORKER_SHUTDOWN_TIMEOUT = CELERY_WORKER_SOFT_SHUTDOWN_TIMEOUT

CELERY_BEAT_SCHEDULE = {
    "heartbeat": {
        "task": "apps.core.tasks.heartbeat",
        "schedule": crontab(minute="*/5"),
    },
    "ocpp_configuration_check": {
        "task": "apps.ocpp.tasks.schedule_daily_charge_point_configuration_checks",
        "schedule": crontab(minute=0, hour=0),
    },
    "ocpp_power_projection": {
        "task": "apps.ocpp.tasks.schedule_power_projection_requests",
        "schedule": crontab(minute=0, hour=1),
    },
    "ocpp_firmware_snapshot": {
        "task": "apps.ocpp.tasks.schedule_daily_firmware_snapshot_requests",
        "schedule": crontab(minute=30, hour=0),
    },
    "ocpp_forwarding_push": {
        "task": "apps.ocpp.tasks.setup_forwarders",
        "schedule": timedelta(minutes=5),
    },
    "ocpp_meter_value_purge": {
        "task": "apps.ocpp.tasks.purge_meter_values",
        "schedule": crontab(minute=0, hour=3),
    },
    "web_request_sampling": {
        "task": "apps.content.tasks.run_scheduled_web_samplers",
        "schedule": timedelta(minutes=1),
    },
}
