# Mnemosyne nginx — single virtual host fronting the Django web app and the
# FastMCP server. HAProxy on Titania terminates TLS; this nginx is plain HTTP
# on the internal 10.10.0.0/24 network.
#
# Routing model
# -------------
# Everything proxies to the Django `app` container by default. The only paths
# NOT sent to Django are:
#   /mcp/          → FastMCP ASGI server (Streamable HTTP + SSE)
#   /static/       → served directly from the shared volume
#   /media/        → served directly from the shared media volume
#   /healthz       → short-circuit proxy to FastMCP /mcp/health for HAProxy
#   /nginx_status  → nginx stub_status (Prospero scrape)
# Django returns its own themed 404 for anything it doesn't route.
#
# Conventions followed (Red Panda Standards + Athena reference config):
#   * `resolver` + variable-based `proxy_pass` so container restarts don't
#     leave nginx caching a dead IP and returning 502 until a full reload.
#   * `$proxy_x_forwarded_proto` preserves HAProxy's `X-Forwarded-Proto: https`
#     so `request.is_secure()`, secure cookies, and `build_absolute_uri()`
#     work correctly behind TLS termination on Titania.
#   * Probe paths are suppressed from access log; the `access_log off` line
#     is needed to defeat the default in nginx:alpine's http-block config
#     (server-level `access_log` is additive, not overriding, so without
#     `off` first the probe paths still log via the inherited directive).
#   * `/live/`, `/ready/`, `/healthz`, `/metrics`, `/nginx_status` are all
#     IP-allowlisted to RFC1918 + loopback — defence in depth even though
#     HAProxy is already on the internal network.

# Docker's embedded DNS — forces nginx to re-resolve upstream hostnames when
# containers restart and get new IPs. Without this, nginx caches the first
# resolution at startup and returns 502 after `docker compose restart app`.
resolver 127.0.0.11 valid=10s;

# Preserve X-Forwarded-Proto from the upstream reverse proxy (HAProxy TLS
# termination on Titania); fall back to $scheme only if there's no upstream
# header. Inside the compose network $scheme is always `http` because HAProxy
# already terminated TLS, so we MUST honour the incoming header.
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
    default $http_x_forwarded_proto;
    ""      $scheme;
}

# Probe-path access-log filter. Genuine 4xx/5xx on these paths still surface
# via the error log and via the probe itself failing.
map $request_uri $loggable {
    default                     1;
    ~^/live(/|\?|$)             0;
    ~^/ready(/|\?|$)            0;
    ~^/metrics(/|\?|$)          0;
    ~^/healthz(/|\?|$)          0;
    ~^/nginx_status(/|\?|$)     0;
    ~^/health                   0;
    ~^/mcp/health(/|\?|$)       0;
    ~^/ping(/|\?|$)             0;
}

# Disable the default nginx.conf access_log (inherited from the http block
# in nginx:alpine's /etc/nginx/nginx.conf) and replace it with the filtered
# version. Without `off` first, both directives fire and probe paths still
# log through the inherited rule.
access_log off;
access_log /dev/stdout combined if=$loggable;

server {
    listen 80 default_server;
    server_name _;

    # Reasonable limits — ingest can upload large content, but the bulk
    # path is S3-direct from Daedalus. 64 MB covers admin uploads and
    # direct REST POST /library/api/items/upload.
    client_max_body_size 64m;
    client_body_timeout  120s;

    # Variable-based upstreams force nginx to re-resolve via the Docker
    # DNS on each request rather than caching the IP at startup.
    set $backend_app http://app:8000;
    set $backend_mcp http://mcp:8001;

    # ── Security headers ──────────────────────────────────────────────────
    # `always` so they apply to 4xx/5xx responses from upstream too.
    # Stronger policies (CSP, Referrer-Policy, HSTS) should be set on
    # HAProxy so they're consistent across all backends.
    add_header X-Frame-Options        "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff"    always;
    add_header X-XSS-Protection       "1; mode=block" always;

    # ── HAProxy liveness for the MCP server ───────────────────────────────
    # Short-circuits to FastMCP's /mcp/health without hitting Django.
    # HAProxy on Titania and any internal uptime monitor use this.
    location = /healthz {
        allow 127.0.0.0/8;     # loopback
        allow 10.0.0.0/8;      # RFC1918 — primary internal range (Incus, HAProxy)
        allow 172.16.0.0/12;   # RFC1918 — Docker bridge networks
        allow 192.168.0.0/16;  # RFC1918
        deny  all;

        proxy_pass $backend_mcp/mcp/health;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
    }

    # ── Static files ──────────────────────────────────────────────────────
    # Baked into the image at /app/staticfiles, seeded into the shared
    # volume by the static-init one-shot service on every `up`.
    location /static/ {
        alias /var/www/static/;
        access_log off;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # ── Media files ──────────────────────────────────────────────────────
    # Empty in production (USE_LOCAL_STORAGE=False → S3Boto3Storage).
    # Useful in staging / dev compose runs where FileSystemStorage writes
    # into MEDIA_ROOT inside the shared volume.
    location /media/ {
        alias /var/www/media/;
        access_log off;
        expires 7d;
        add_header Cache-Control "public";
    }

    # ── FastMCP server ────────────────────────────────────────────────────
    # Streamable HTTP at /mcp/ and SSE at /mcp/sse/. Long-running streams
    # require disabled buffering, an unset Connection header (so HTTP/1.1
    # keep-alive stays upstream), and a generous read timeout.
    location /mcp/ {
        proxy_pass $backend_mcp;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
        proxy_set_header Authorization     $http_authorization;
        proxy_set_header Connection        "";
        proxy_redirect    off;
        proxy_buffering   off;
        proxy_cache       off;
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
    }

    # ── Prometheus scrape — internal networks only ────────────────────────
    # /metrics itself is owned by Django's django-prometheus middleware.
    # Omitting 10.0.0.0/8 silently breaks scrapes from Prospero in the
    # Incus network; omitting 172.16.0.0/12 breaks scrapes from a
    # Prometheus container on the default Docker bridge.
    location = /metrics {
        allow 127.0.0.0/8;
        allow 10.0.0.0/8;
        allow 172.16.0.0/12;
        allow 192.168.0.0/16;
        deny  all;

        proxy_pass $backend_app;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
        access_log off;
    }

    # ── nginx stub_status — internal networks only ────────────────────────
    # Cheap endpoint for Prospero to watch active connections + request rate.
    location = /nginx_status {
        allow 127.0.0.0/8;
        allow 10.0.0.0/8;
        allow 172.16.0.0/12;
        allow 192.168.0.0/16;
        deny  all;

        stub_status on;
        access_log off;
    }

    # ── Health probes on the Django app ───────────────────────────────────
    # Use the trailing-slash form: /live/ and /ready/ return 200 directly.
    # The un-slashed forms trigger Django's APPEND_SLASH 301 redirect, so
    # clients that don't follow redirects will report a failure even when
    # the service is healthy.
    location = /live/ {
        allow 127.0.0.0/8;
        allow 10.0.0.0/8;
        allow 172.16.0.0/12;
        allow 192.168.0.0/16;
        deny  all;

        proxy_pass $backend_app;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
        access_log off;
    }

    location = /ready/ {
        allow 127.0.0.0/8;
        allow 10.0.0.0/8;
        allow 172.16.0.0/12;
        allow 192.168.0.0/16;
        deny  all;

        proxy_pass $backend_app;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
        access_log off;
    }

    # ── Catch-all: everything else goes to Django ─────────────────────────
    # Django owns `/`, `/dashboard/`, `/login/`, `/logout/`, `/profile/*`,
    # `/notifications/*`, `/admin/*`, `/library/*`, `/llm/*`, `/api/v1/*`,
    # and any future app URL. It returns its own themed 404 for anything
    # unrouted — not nginx's default page.
    location / {
        proxy_pass $backend_app;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
        proxy_redirect    off;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}
