"""
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/
"""

from pathlib import Path
import contextlib
import os
import sys
import ipaddress
import socket
from core.log_paths import select_log_dir
from django.utils.translation import gettext_lazy as _
from celery.schedules import crontab
from django.http import 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 config.settings_helpers import (
    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

ACRONYMS: list[str] = []
with contextlib.suppress(FileNotFoundError):
    ACRONYMS = [
        line.strip()
        for line in (BASE_DIR / "config" / "data" / "ACRONYMS.txt")
        .read_text()
        .splitlines()
        if line.strip()
    ]


# 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"

def _env_bool(name: str, default: bool) -> bool:
    value = os.environ.get(name)
    if value is None:
        return default

    normalized = value.strip().lower()
    if normalized in {"1", "true", "yes", "on"}:
        return True
    if normalized in {"0", "false", "no", "off"}:
        return False
    return default


DEBUG = _env_bool("DEBUG", False)

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


_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)


# 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 request.is_secure():
        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 _candidate_origin_tuples(request, allowed_hosts: list[str]) -> list[tuple[str, str, str | None]]:
    default_scheme = _get_request_scheme(request)
    candidates: list[tuple[str, str, str | None]] = []
    seen: set[tuple[str, str, str | None]] = set()

    def _append_candidate(scheme: str | None, host: str) -> None:
        if not scheme or not host:
            return
        normalized = _normalize_origin_tuple(scheme, host)
        if normalized is None:
            return
        if not _host_is_allowed(host, allowed_hosts):
            return
        if normalized in seen:
            return
        candidates.append(normalized)
        seen.add(normalized)

    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)
        _append_candidate(scheme, host)

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

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

    return candidates


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)

    origin_ip = extract_ip_from_host(normalized_origin[1])

    for candidate in _candidate_origin_tuples(request, allowed_hosts):
        if candidate == normalized_origin:
            return True

        candidate_ip = extract_ip_from_host(candidate[1])
        if origin_ip and candidate_ip:
            for pattern in allowed_hosts:
                try:
                    network = ipaddress.ip_network(pattern)
                except ValueError:
                    continue
                if origin_ip in network and candidate_ip in network:
                    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()
    referer_ip = extract_ip_from_host(normalized_referer[1])

    for candidate in _candidate_origin_tuples(request, allowed_hosts):
        if candidate == normalized_referer:
            return

        candidate_ip = extract_ip_from_host(candidate[1])
        if referer_ip and candidate_ip:
            for pattern in allowed_hosts:
                try:
                    network = ipaddress.ip_network(pattern)
                except ValueError:
                    continue
                if referer_ip in network and candidate_ip in network:
                    return

    return _original_check_referer(self, request)


CsrfViewMiddleware._check_referer = _check_referer_with_forwarded


# Application definition

LOCAL_APPS = [
    "nodes",
    "core",
    "ocpp",
    "awg",
    "pages",
    "teams",
]

INSTALLED_APPS = [
    "whitenoise.runserver_nostatic",
    "django.contrib.admin",
    "django.contrib.admindocs",
    "django_otp",
    "django_otp.plugins.otp_totp",
    "config.auth_app.AuthConfig",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "import_export",
    "django_object_actions",
    "django.contrib.sites",
    "channels",
    "config.horologia_app.HorologiaConfig",
] + LOCAL_APPS

if DEBUG:
    try:
        import debug_toolbar  # type: ignore
    except ModuleNotFoundError:  # pragma: no cover - optional dependency
        pass
    else:
        INSTALLED_APPS += ["debug_toolbar"]

SITE_ID = 1

_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

        if request is not None and isinstance(exc, Site.DoesNotExist):
            return RequestSite(request)
        raise


sites_shortcuts.get_current_site = _get_current_site_with_request_fallback

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "config.middleware.ActiveAppMiddleware",
    "config.middleware.SiteHttpsRedirectMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django_otp.middleware.OTPMiddleware",
    "core.middleware.AdminHistoryMiddleware",
    "core.middleware.SigilContextMiddleware",
    "pages.middleware.ViewHistoryMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

if DEBUG:
    try:
        import debug_toolbar  # type: ignore
    except ModuleNotFoundError:  # pragma: no cover - optional dependency
        pass
    else:
        MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
        INTERNAL_IPS = ["127.0.0.1", "localhost"]

CSRF_FAILURE_VIEW = "pages.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 / "pages" / "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",
                "pages.context_processors.nav_links",
                "config.context_processors.site_and_node",
            ],
        },
    },
]

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

# Channels configuration
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}


# MCP sigil resolver configuration
def _env_int(name: str, default: int) -> int:
    try:
        return int(os.environ.get(name, default))
    except (TypeError, ValueError):  # pragma: no cover - defensive
        return default


def _split_env_list(name: str) -> list[str]:
    raw = os.environ.get(name)
    if not raw:
        return []
    return [item.strip() for item in raw.split(",") if item.strip()]


MCP_SIGIL_SERVER = {
    "host": os.environ.get("MCP_SIGIL_HOST", "127.0.0.1"),
    "port": _env_int("MCP_SIGIL_PORT", 8800),
    "api_keys": _split_env_list("MCP_SIGIL_API_KEYS"),
    "required_scopes": ["sigils:read"],
    "issuer_url": os.environ.get("MCP_SIGIL_ISSUER_URL"),
    "resource_server_url": os.environ.get("MCP_SIGIL_RESOURCE_URL"),
    "mount_path": os.environ.get("MCP_SIGIL_MOUNT_PATH", "/mcp"),
}


# Custom user model
AUTH_USER_MODEL = "core.User"

# Enable RFID authentication backend and restrict default admin login to localhost
AUTHENTICATION_BACKENDS = [
    "core.backends.TempPasswordBackend",
    "core.backends.LocalhostAdminBackend",
    "core.backends.TOTPBackend",
    "core.backends.RFIDBackend",
]

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

# Issuer name used when generating otpauth URLs for authenticator apps.
OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")

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

FORCED_DB_BACKEND = os.environ.get("ARTHEXIS_FORCE_DB_BACKEND", "").strip().lower()
if FORCED_DB_BACKEND and FORCED_DB_BACKEND not in {"sqlite", "postgres"}:
    raise ImproperlyConfigured(
        "ARTHEXIS_FORCE_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"

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


# 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")),
]

LOCALE_PATHS = [BASE_DIR / "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"

# 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
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

# 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 = select_log_dir(BASE_DIR)
os.environ.setdefault("ARTHEXIS_LOG_DIR", str(LOG_DIR))
OLD_LOG_DIR = LOG_DIR / "old"
OLD_LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE_NAME = "tests.log" if "test" in sys.argv else f"{socket.gethostname()}.log"

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        }
    },
    "handlers": {
        "file": {
            "class": "config.logging.ActiveAppFileHandler",
            "filename": str(LOG_DIR / LOG_FILE_NAME),
            "when": "midnight",
            "backupCount": 7,
            "encoding": "utf-8",
            "formatter": "standard",
        },
        "error_file": {
            "class": "config.logging.ErrorFileHandler",
            "filename": str(LOG_DIR / "error.log"),
            "when": "midnight",
            "backupCount": 7,
            "encoding": "utf-8",
            "formatter": "standard",
            "level": "ERROR",
        },
        "console": {
            "class": "logging.StreamHandler",
            "level": "ERROR",
            "formatter": "standard",
        },
    },
    "root": {
        "handlers": ["file", "error_file", "console"],
        "level": "DEBUG",
    },
}


# Celery configuration
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "memory://")
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "cache+memory://")
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"

CELERY_BEAT_SCHEDULE = {
    "heartbeat": {
        "task": "core.tasks.heartbeat",
        "schedule": crontab(minute="*/5"),
    },
}
