"""
Django settings for config project.

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

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

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

import os
from pathlib import Path

from dotenv import load_dotenv

# Build paths inside the project like this: BASE_DIR / 'subdir'.
# Three parents, not two: this file is config/settings/base.py, so
# parent=settings, parent.parent=config, parent.parent.parent=<project root>.
BASE_DIR = Path(__file__).resolve().parent.parent.parent

load_dotenv(BASE_DIR / '.env')


def _env_bool(name: str, default: bool) -> bool:
    val = os.environ.get(name)
    if val is None:
        return default
    return val.strip().lower() in ('1', 'true', 'yes', 'on')


def _env_int(name: str, default: int) -> int:
    val = os.environ.get(name)
    if val is None:
        return default
    try:
        return int(val)
    except ValueError:
        return default


SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
if not SECRET_KEY:
    raise RuntimeError('DJANGO_SECRET_KEY env var is required')

DEBUG = _env_bool('DJANGO_DEBUG', False)

ALLOWED_HOSTS = [h.strip() for h in os.environ.get('DJANGO_ALLOWED_HOSTS', 'api.caridu.id,localhost,127.0.0.1').split(',') if h.strip()]


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'drf_spectacular',
    'channels',
    'axes',
    'django_celery_beat',
    'apps.core',
    'apps.hr',
    'apps.projects',
    'apps.events',
    'apps.meetings',
    'apps.tasks',
    'apps.asset_management',
    'apps.procurements',
    'apps.administrations',
    'apps.publications',
    'apps.contacts',
    'apps.companies',
    'apps.activities',
    'apps.chat',
    'apps.chatbot',
    'apps.tools',
    'apps.notifications',
    'apps.scheduler',
    'apps.analytics',
    'apps.disseminations',
    'apps.finance',
    'apps.accounting',
    'apps.donors',
]

MIDDLEWARE = [
    # PERF-006: first in, last out — wraps every other middleware + the view
    # so its measured duration is the actual end-to-end request time.
    'apps.core.middleware.RequestObservabilityMiddleware',
    'django.middleware.security.SecurityMiddleware',
    # GZip must run before any middleware that may set Vary or modify content
    'django.middleware.gzip.GZipMiddleware',
    # WhiteNoise serves STATIC_ROOT and (via subclass) MEDIA_ROOT directly from WSGI
    'config.whitenoise_media.WhiteNoiseWithMedia',
    'corsheaders.middleware.CorsMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # Directly after AuthenticationMiddleware: revokes live sessions whose
    # account was suspended or locked after the session was created.
    'apps.core.middleware.AccountStatusMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    # AxesMiddleware must be last so it can record outcome of every request
    'axes.middleware.AxesMiddleware',
    # Analytics access log: capture public hits on tracked resources.
    'apps.analytics.middleware.AccessLogMiddleware',
]

# PERF-006: request observability (apps.core.middleware.RequestObservabilityMiddleware)
REQUEST_SLOW_THRESHOLD_MS = _env_int('REQUEST_SLOW_THRESHOLD_MS', 1000)
# Opt-in: force_debug_cursor has real overhead, so DB query count/time on
# slow requests is off by default — enable per deployment when needed.
REQUEST_OBSERVABILITY_DB_STATS = _env_bool('REQUEST_OBSERVABILITY_DB_STATS', False)

# Public access analytics
ANALYTICS_ENABLED = _env_bool('ANALYTICS_ENABLED', True)
ANALYTICS_HASH_SALT = os.environ.get('ANALYTICS_HASH_SALT', '')  # falls back to SECRET_KEY
ANALYTICS_SKIP_PREFIXES = ['/admin/', '/api/schema', '/api/auth/']
# Absolute base URL the embeddable tracking tag points at (no trailing slash).
ANALYTICS_PUBLIC_BASE_URL = os.environ.get('ANALYTICS_PUBLIC_BASE_URL', 'http://localhost:8000')
# Number of trusted reverse-proxy hops in front of the app. 0 = ignore X-Forwarded-For
# (use REMOTE_ADDR) so clients can't spoof their IP. Set to match the deploy (e.g. 1 behind
# a single proxy, 2 behind Cloudflare->cPanel) to recover real client IPs for analytics/throttling.
TRUSTED_PROXY_COUNT = int(os.environ.get('TRUSTED_PROXY_COUNT', '0'))
# Only honor Cloudflare CF-IPCountry* headers when traffic is actually forced through Cloudflare.
BEHIND_CLOUDFLARE = _env_bool('BEHIND_CLOUDFLARE', False)
# Absolute base URL for public short links (the trackable link shared in posts).
# Defaults to the analytics base; short links resolve at <base>/api/urls/<code>/.
SHORTLINK_BASE_URL = os.environ.get('SHORTLINK_BASE_URL', ANALYTICS_PUBLIC_BASE_URL)

# Firebase Cloud Messaging (web push). Point FIREBASE_CREDENTIALS_FILE at a
# service-account JSON kept OUTSIDE the repo (e.g. ~/secrets/firebase.json on
# cPanel). Pushes are skipped silently when unset, so dev/CI run without creds.
FCM_ENABLED = _env_bool('FCM_ENABLED', True)
FIREBASE_CREDENTIALS_FILE = os.environ.get('FIREBASE_CREDENTIALS_FILE', '')

# Worker count for the fire-and-forget pool in apps.core.background (FCM,
# SMTP fan-out). Raise on hosts with headroom; tasks are pure network I/O.
BACKGROUND_MAX_WORKERS = int(os.environ.get('BACKGROUND_MAX_WORKERS', '4'))

AUTHENTICATION_BACKENDS = [
    'axes.backends.AxesStandaloneBackend',
    # Replaces the stock ModelBackend so suspended/locked account_status values
    # actually block authentication (SEC-12).
    'apps.core.auth_backends.AccountStatusModelBackend',
]

# Brute-force protection (django-axes)
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 1  # hours
AXES_LOCKOUT_PARAMETERS = [['username', 'ip_address']]
AXES_RESET_ON_SUCCESS = True
AXES_LOCKOUT_CALLABLE = None
AXES_ENABLE_ADMIN = True
# Behind a reverse proxy in prod, set AXES_IPWARE_PROXY_COUNT or use X-Forwarded-For trust list
AXES_IPWARE_META_PRECEDENCE_ORDER = ['HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR']

CORS_ALLOW_CREDENTIALS = True
# One codebase, several deployments (api.caridu.id, api.nocthink.com), so the
# fallback must NOT name a specific production frontend: a host that forgot to
# set CORS_ALLOWED_ORIGINS would otherwise trust another deployment's origin
# with credentials attached. Localhost only by default; every deployed host
# sets this explicitly in its env file.
CORS_ALLOWED_ORIGINS = [o.strip() for o in os.environ.get(
    'CORS_ALLOWED_ORIGINS',
    'http://localhost:3000,http://127.0.0.1:3000,http://localhost:3010,http://127.0.0.1:3010'
).split(',') if o.strip()]

# Cookie security: HTTPS-only in prod, HTTPOnly always, SameSite env-driven.
# No default cookie Domain: the SPA is served from orm.csis.or.id while the API
# is api.caridu.id — different registrable domains, so a '.caridu.id' Domain
# attribute would stop the browser storing the cookie at all. Host-only cookies
# on api.caridu.id work for cross-site XHR provided SameSite=None; Secure.
# Set SESSION_COOKIE_DOMAIN / CSRF_COOKIE_DOMAIN explicitly if a deployment ever
# needs cookies shared across caridu.id subdomains again.
_default_cookie_domain = None

# cached_db: session reads hit the cache first with write-through to DB —
# saves one DB query per request once warm. Safe with LocMemCache (a cache
# miss just falls back to the DB read).
SESSION_ENGINE = os.environ.get(
    'SESSION_ENGINE', 'django.contrib.sessions.backends.cached_db'
)
SESSION_COOKIE_NAME = os.environ.get('SESSION_COOKIE_NAME', 'sessionid')
# Cross-site by default in prod: orm.csis.or.id -> api.caridu.id. 'Lax' would
# make the browser withhold the cookie on those XHRs entirely. Requires Secure.
SESSION_COOKIE_SAMESITE = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax' if DEBUG else 'None')
SESSION_COOKIE_SECURE = _env_bool('SESSION_COOKIE_SECURE', not DEBUG)
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_AGE = 60 * 60 * 24 * 14  # 14 days
SESSION_COOKIE_DOMAIN = os.environ.get('SESSION_COOKIE_DOMAIN') or _default_cookie_domain

CSRF_COOKIE_NAME = os.environ.get('CSRF_COOKIE_NAME', 'csrftoken')
CSRF_COOKIE_SAMESITE = os.environ.get('CSRF_COOKIE_SAMESITE', 'Lax' if DEBUG else 'None')
CSRF_COOKIE_SECURE = _env_bool('CSRF_COOKIE_SECURE', not DEBUG)
CSRF_COOKIE_HTTPONLY = False  # JS must read for double-submit header
CSRF_COOKIE_DOMAIN = os.environ.get('CSRF_COOKIE_DOMAIN') or _default_cookie_domain

# Localhost-only fallback, for the same reason as CORS_ALLOWED_ORIGINS above:
# a deployment that forgot this setting must not trust a sibling deployment's
# origin for state-changing requests.
CSRF_TRUSTED_ORIGINS = [o.strip() for o in os.environ.get(
    'CSRF_TRUSTED_ORIGINS',
    'http://localhost:3000,http://localhost:8000,http://127.0.0.1:3000,http://127.0.0.1:8000,http://localhost:3010,http://127.0.0.1:3010'
).split(',') if o.strip()]

# HTTPS + reverse-proxy hardening.
# Production sits behind Apache/cPanel proxy: api.caridu.id → upstream WSGI.
# Trust X-Forwarded-* so Django builds absolute URLs and cookie domains using
# the public hostname, not the upstream's Host header.
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') if not DEBUG else None
USE_X_FORWARDED_HOST = not DEBUG
USE_X_FORWARDED_PORT = not DEBUG
SECURE_HSTS_SECONDS = 31536000 if not DEBUG else 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = not DEBUG
SECURE_HSTS_PRELOAD = not DEBUG
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = 'same-origin'
X_FRAME_OPTIONS = 'DENY'

REST_FRAMEWORK = {
    # HasMenuPermission is a no-op for ViewSets that don't set rbac_domain,
    # so tools/pdf_tools/chat/chatbot/notifications/core endpoints stay open to any auth user.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
        'apps.core.permissions.HasMenuPermission',
    ],
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '30/min',
        'user': '600/min',
        'login': '10/min',
        'chat': '20/min',
        'link_preview': '30/min',
    },
}

SPECTACULAR_SETTINGS = {
    'TITLE': 'KHub CRM API',
    'DESCRIPTION': 'CRM backend API with full feature coverage',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,
}

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

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

# Redis URL drives both the Channels layer and the cache below. Set it for
# local/dev (or any ASGI host with Redis); leave unset on prod cPanel (WSGI,
# no Redis) to fall back to in-memory backends.
_redis_url = os.environ.get('REDIS_URL', '').strip()

# WebSocket delivery. Off by default: production runs on cPanel, which is
# WSGI-only, so no ASGI worker ever serves /ws/ and every browser connection
# 404s (166 such attempts in one production log). Notifications and chat are
# delivered by polling instead — see refetchInterval in the frontend
# notification/chat hooks, which is the real transport, not a fallback.
#
# When False, NotificationHandler._broadcast_notification and
# ChatViewSet._broadcast_new_message return early rather than serializing a
# payload into a channel layer nobody reads.
#
# Re-enabling on an ASGI host needs BOTH this and the frontend's
# VITE_ENABLE_WEBSOCKET / NEXT_PUBLIC_ENABLE_WEBSOCKET flipped together: one
# without the other gives a client connecting to a server that never
# broadcasts, or a server broadcasting to nobody. Both failures are silent.
WEBSOCKET_ENABLED = _env_bool('WEBSOCKET_ENABLED', False)

# Channel layer (WebSocket fan-out). Redis when REDIS_URL is set, else an
# in-memory layer so a host without Redis still boots. Left configured even
# when WEBSOCKET_ENABLED is False: it is lazy, so with both broadcast sites
# gated nothing ever opens a connection.
if _redis_url:
    # Same RESP3 caveat as CACHES below: channels-redis uses redis-py too, so a
    # pre-6.0 server rejects its `HELLO 3` handshake and the layer never
    # connects. Kept in sync via the same env var.
    _resp_protocol = int(os.environ.get('REDIS_RESP_PROTOCOL', '3'))
    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels_redis.core.RedisChannelLayer",
            "CONFIG": {
                "hosts": [{"address": _redis_url, "protocol": _resp_protocol}],
            },
        },
    }
else:
    CHANNEL_LAYERS = {
        "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"},
    }


# Cache
# Use Redis when REDIS_URL is set (local/dev with a Redis server, or any host
# that provisions one). Falls back to per-process local memory otherwise so
# prod cPanel (WSGI-only, no Redis) keeps working unchanged.
# https://docs.djangoproject.com/en/6.0/topics/cache/
if _redis_url:
    CACHES = {
        'default': {
            # Resilient subclass: DRF throttling hits the cache on every request
            # before permissions run, so an unreachable Redis would otherwise
            # 500 login/CSRF. See apps.core.cache_backends for the trade-off.
            'BACKEND': 'apps.core.cache_backends.ResilientRedisCache',
            'LOCATION': _redis_url,
            'KEY_PREFIX': 'khub',
            'OPTIONS': {
                # Without these, a hung (not refused) Redis blocks the request
                # thread on redis-py's default of no timeout.
                'socket_connect_timeout': 1,
                'socket_timeout': 1,
                # redis-py >= 5.3 defaults to RESP3 and opens every connection
                # with `HELLO 3`, which a Redis server older than 6.0 rejects:
                #   ResponseError: unknown command `HELLO`
                # ResilientRedisCache then degrades every single get/set, so the
                # cache silently has a 0% hit rate (observed on api.nocthink.com:
                # 12,964 cache_unavailable warnings, sessions falling through to
                # the database on every request). Pin RESP2 where the server is
                # old; drop REDIS_RESP_PROTOCOL once it is on Redis >= 6.
                'protocol': int(os.environ.get('REDIS_RESP_PROTOCOL', '3')),
            },
        }
    }
else:
    CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
            'LOCATION': 'khub-locmem',
        }
    }


# Celery — reminder/scheduled-notification tasks (apps.scheduler). Uses the
# same Redis instance as the channel layer/cache (REDIS_URL); a host without
# Redis simply can't run the worker/beat processes, same tradeoff as
# CHANNEL_LAYERS above.
#
# Unix-socket Redis (cPanel serves one per account, e.g.
# unix:///home/<acct>/redis.sock) needs a different URL scheme per library:
# redis-py — used by the cache and channels-redis — parses 'unix://', while
# Celery's kombu only registers a 'redis+socket://' transport and raises
# "No such transport: unix" on the former. Translate rather than making the
# operator set two env vars that must stay in sync.
if _redis_url.startswith('unix://'):
    _path, _, _query = _redis_url[len('unix://'):].partition('?')
    # kombu spells the database 'virtual_host', not 'db'.
    _db = ''
    for _param in _query.split('&'):
        if _param.startswith('db='):
            _db = f'?virtual_host={_param[3:]}'
    CELERY_BROKER_URL = f'redis+socket://{_path}{_db}'
else:
    CELERY_BROKER_URL = _redis_url or 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = None
CELERY_TASK_ALWAYS_EAGER = _env_bool('CELERY_TASK_ALWAYS_EAGER', False)
CELERY_TIMEZONE = 'UTC'  # matches TIME_ZONE below
CELERY_TASK_TRACK_STARTED = True

# PERF-004: concurrency/prefetch/time limits are explicit, not left to
# Celery's own defaults (which read host CPU count — unreliable under a
# container's CPU quota/memory budget). Defaults below are the conservative
# low-resource starting point; raise only from measured throughput and RSS
# on the actual deployment target (see task.md Phase 6's deployment
# resource worksheet — not filled in yet, no production numbers available
# to this pass).
CELERY_WORKER_CONCURRENCY = _env_int('CELERY_WORKER_CONCURRENCY', 1)
CELERY_WORKER_PREFETCH_MULTIPLIER = _env_int('CELERY_WORKER_PREFETCH_MULTIPLIER', 1)
CELERY_TASK_SOFT_TIME_LIMIT = _env_int('CELERY_TASK_SOFT_TIME_LIMIT', 300)  # 5 min
CELERY_TASK_TIME_LIMIT = _env_int('CELERY_TASK_TIME_LIMIT', 600)  # 10 min hard kill
CELERY_WORKER_MAX_TASKS_PER_CHILD = _env_int('CELERY_WORKER_MAX_TASKS_PER_CHILD', 200)


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

_db_engine = os.environ.get('DB_ENGINE', 'sqlite').lower()

# Persistent connections: reuse a DB connection for up to CONN_MAX_AGE seconds
# instead of opening one per request. Big latency win under load. Keep 0 only
# if the deploy can't hold open connections.
_conn_max_age = int(os.environ.get('DB_CONN_MAX_AGE', '60'))

if _db_engine in ('postgres', 'postgresql'):
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': os.environ['DB_NAME'],
            'USER': os.environ['DB_USER'],
            'PASSWORD': os.environ['DB_PASSWORD'],
            'HOST': os.environ.get('DB_HOST', 'localhost'),
            'PORT': os.environ.get('DB_PORT', '5432'),
            'CONN_MAX_AGE': _conn_max_age,
            # Reuse connections only while healthy (Django 4.1+).
            'CONN_HEALTH_CHECKS': True,
        }
    }
elif _db_engine == 'mysql':
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': os.environ['DB_NAME'],
            'USER': os.environ['DB_USER'],
            'PASSWORD': os.environ['DB_PASSWORD'],
            'HOST': os.environ.get('DB_HOST', 'localhost'),
            'PORT': os.environ.get('DB_PORT', '3306'),
            'OPTIONS': {
                'charset': 'utf8mb4',
                'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
            },
            'CONN_MAX_AGE': _conn_max_age,
            'CONN_HEALTH_CHECKS': True,
        }
    }
else:
    # sqlite: CONN_MAX_AGE has little effect (file-based) but harmless.
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': BASE_DIR / 'db.sqlite3',
        }
    }


# Password validation
# https://docs.djangoproject.com/en/6.0/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/6.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static + media files
# https://docs.djangoproject.com/en/6.0/howto/static-files/
# Apache serves STATIC_ROOT and MEDIA_ROOT directly from
# /home/csis3web/network.csis.or.id/{static,media}.

STATIC_URL = os.environ.get('DJANGO_STATIC_URL', '/static/')
STATIC_ROOT = os.environ.get(
    'DJANGO_STATIC_ROOT',
    '/home/csis3web/network.csis.or.id/static',
)

# Whether MEDIA_ROOT is served without authentication (WhiteNoise + the DEBUG
# static route). True preserves the historical behavior; False routes all reads
# through /api/media/<path>, which checks the caller's organization.
#
# Defaults True so caridu keeps working unchanged — flipping it breaks every
# existing <img src> and download link until that frontend is updated. The
# nocthink deployment sets it False (see config/settings/nocthink.py).
MEDIA_SERVE_UNAUTHENTICATED = _env_bool('MEDIA_SERVE_UNAUTHENTICATED', True)

# Product name shown to users in email subjects and bodies. One codebase serves
# two products, so this must never be hardcoded in a template or view: caridu
# users would see "Nocthink" and vice versa. Defaults to the caridu name, which
# is what shipped before this setting existed.
PRODUCT_NAME = os.environ.get('DJANGO_PRODUCT_NAME', 'K-Hub').strip() or 'K-Hub'

MEDIA_URL = os.environ.get('DJANGO_MEDIA_URL', '/media/')
MEDIA_ROOT = os.environ.get(
    'DJANGO_MEDIA_ROOT',
    '/home/csis3web/network.csis.or.id/media',
)

# Email. Defaults to console backend (prints to stdout) for dev; set
# DJANGO_EMAIL_BACKEND=anymail.backends.microsoft_graph.EmailBackend in
# production to send via Office365 through Microsoft Graph (app-only OAuth2,
# no SMTP password). Reuses the MICROSOFT_* app registration below — the app
# needs the Mail.Send *application* permission with admin consent.
EMAIL_BACKEND = os.environ.get(
    'DJANGO_EMAIL_BACKEND',
    'django.core.mail.backends.console.EmailBackend',
)
DEFAULT_FROM_EMAIL = os.environ.get('DJANGO_DEFAULT_FROM_EMAIL', 'Nalar <no-reply@karajohub.local>')

ANYMAIL = {
    "MICROSOFT_GRAPH_CLIENT_ID": os.environ.get('MICROSOFT_CLIENT_ID', ''),
    "MICROSOFT_GRAPH_CLIENT_SECRET": os.environ.get('MICROSOFT_CLIENT_SECRET', ''),
    "MICROSOFT_GRAPH_TENANT_ID": os.environ.get('MICROSOFT_TENANT', ''),
    # Mailbox the app sends as (e.g. noreply@csis.or.id). Must match a real
    # Office365 mailbox the app registration has Mail.Send rights on.
    "MICROSOFT_GRAPH_SENDER": os.environ.get('MICROSOFT_GRAPH_SENDER', ''),
}

# Cloudflare Email Sending (REST). Select with
# DJANGO_EMAIL_BACKEND=apps.core.email_backends.CloudflareEmailBackend
# The sending domain must be onboarded first (`wrangler email sending enable
# <domain>`), and DEFAULT_FROM_EMAIL must be on that domain or every send 400s.
# Token needs the email sending permission — scope it to that alone.
CLOUDFLARE_ACCOUNT_ID = os.environ.get('CLOUDFLARE_ACCOUNT_ID', '').strip()
CLOUDFLARE_EMAIL_API_TOKEN = os.environ.get('CLOUDFLARE_EMAIL_API_TOKEN', '').strip()
CLOUDFLARE_EMAIL_TIMEOUT = int(os.environ.get('CLOUDFLARE_EMAIL_TIMEOUT', '10'))
CLOUDFLARE_EMAIL_MAX_RETRIES = int(os.environ.get('CLOUDFLARE_EMAIL_MAX_RETRIES', '2'))

# WhiteNoise (static + media via WhiteNoiseWithMedia subclass).
# AUTOREFRESH: rescan disk per request so newly uploaded media is served
# without a worker restart. Negligible overhead for bootstrap traffic.
# Set WHITENOISE_AUTOREFRESH=false when Apache/nginx serves /media/ directly.
WHITENOISE_AUTOREFRESH = _env_bool('WHITENOISE_AUTOREFRESH', True)
WHITENOISE_USE_FINDERS = DEBUG
# Don't crash if a hashed asset is missing from the manifest (templates may
# reference legacy paths during deploys). Prod-resilient default.
WHITENOISE_MANIFEST_STRICT = False
# 1-year cache for hashed static assets; WhiteNoise auto-disables for media
# (non-hashed) and serves with conservative defaults.
WHITENOISE_MAX_AGE = 31536000 if not DEBUG else 0

# Cloudflare R2 (S3-compatible) for user-uploaded media. Opt-in: media stays on
# the local filesystem unless all four R2_* values are present, so a host without
# R2 credentials keeps working exactly as before.
R2_ACCOUNT_ID = os.environ.get('R2_ACCOUNT_ID', '').strip()
R2_ACCESS_KEY_ID = os.environ.get('R2_ACCESS_KEY_ID', '').strip()
R2_SECRET_ACCESS_KEY = os.environ.get('R2_SECRET_ACCESS_KEY', '').strip()
R2_BUCKET_NAME = os.environ.get('R2_BUCKET_NAME', '').strip()
# Account-level S3 endpoint. Override only for a custom/jurisdiction endpoint.
R2_ENDPOINT_URL = os.environ.get(
    'R2_ENDPOINT_URL',
    f'https://{R2_ACCOUNT_ID}.r2.cloudflarestorage.com' if R2_ACCOUNT_ID else '',
).strip()
# Presigned GET lifetime. Short by design: the URL is the only thing standing
# between a leak and the object, since the bucket itself is private.
R2_URL_EXPIRY = int(os.environ.get('R2_URL_EXPIRY', '300'))

_r2_enabled = all([
    R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_ENDPOINT_URL,
])

# Django 6 STORAGES: production-grade static pipeline.
# CompressedManifestStaticFilesStorage hashes filenames + gzip/brotli pre-compresses.
STORAGES = {
    'default': {
        'BACKEND': (
            'apps.core.storage_backends.R2MediaStorage'
            if _r2_enabled
            else 'django.core.files.storage.FileSystemStorage'
        ),
    },
    'staticfiles': {
        'BACKEND': (
            'whitenoise.storage.CompressedManifestStaticFilesStorage'
            if not DEBUG
            else 'django.contrib.staticfiles.storage.StaticFilesStorage'
        ),
    },
}

# Upload limits: cap multipart body + per-file size to mitigate DoS via huge uploads
DATA_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024   # 25 MB total request body
FILE_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024   # 25 MB per file in memory
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000

# App-level: hard cap per attachment file (50 MB).
# Used by views before persisting. Tune per deployment via env.
ATTACHMENT_MAX_FILE_SIZE = int(os.environ.get('ATTACHMENT_MAX_FILE_SIZE', 50 * 1024 * 1024))
ATTACHMENT_ALLOWED_MIME_PREFIXES = (
    'image/', 'application/pdf', 'application/zip',
    'application/vnd.openxmlformats-officedocument.',
    'application/msword', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint',
    'text/', 'audio/', 'video/',
)

AUTH_USER_MODEL = 'core.User'

# AWS Rekognition (HR attendance face recognition)
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID', '')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY', '')
AWS_REGION = os.environ.get('AWS_REGION', 'ap-southeast-1')
# Per-environment collection: ExternalImageId is a User uuid, which is not
# portable across databases — a shared collection makes dev faces match prod
# users and vice versa.
REKOGNITION_COLLECTION_ID = os.environ.get('REKOGNITION_COLLECTION_ID', 'khub-attendance-dev')
REKOGNITION_FACE_MATCH_THRESHOLD = float(os.environ.get('REKOGNITION_FACE_MATCH_THRESHOLD', '90'))
# Index the profile photo as the user's face whenever the avatar changes.
REKOGNITION_AVATAR_ENROLLMENT = os.environ.get('REKOGNITION_AVATAR_ENROLLMENT', 'True') == 'True'

# Microsoft Azure AD / Office 365 OAuth
MICROSOFT_CLIENT_ID = os.environ.get('MICROSOFT_CLIENT_ID', '')
MICROSOFT_CLIENT_SECRET = os.environ.get('MICROSOFT_CLIENT_SECRET', '')
MICROSOFT_TENANT = os.environ.get('MICROSOFT_TENANT', 'common')
MICROSOFT_REDIRECT_URI = os.environ.get(
    'MICROSOFT_REDIRECT_URI',
    'http://localhost:8000/api/auth/microsoft/callback/'
)
# Organization a brand-new Microsoft SSO user joins. Empty means "no automatic
# membership": the account is created but joins nothing, and an admin must
# invite it. Previously this fell back to Organization.objects.first(), which
# on a multi-tenant deployment silently placed the user in whichever tenant was
# created first (SEC-11). Never give this a default — the correct tenant is a
# per-deployment fact.
MICROSOFT_SSO_DEFAULT_ORG_ID = os.environ.get('MICROSOFT_SSO_DEFAULT_ORG_ID', '')
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000')

# DeepSeek AI (OpenAI-compatible) — project description assistant
DEEPSEEK_API_KEY = os.environ.get('DEEPSEEK_API_KEY', '')
DEEPSEEK_BASE_URL = os.environ.get('DEEPSEEK_BASE_URL', 'https://api.deepseek.com')
DEEPSEEK_MODEL = os.environ.get('DEEPSEEK_MODEL', 'deepseek-chat')
