# Local-dev infrastructure stack for `apf_portal`.
#
# Spins up the runtime dependencies the BFF and ADRs assume:
#   - PostgreSQL 17           (per ADR-0006)
#   - Redis 7                 (per ADR-0010 — single node in dev,
#                              Sentinel HA in prod)
#   - OpenTelemetry Collector (per ADR-0012 — receives OTLP from the
#                              BFF and the SPA, debug-logs traces in
#                              v1, forwards to Jaeger when activated)
#
# Optional viewers / tooling behind Compose profiles:
#   - --profile dbtools       → pgweb (Postgres GUI)
#   - --profile observability → Jaeger UI (trace explorer)
#   - --profile serve-static  → Caddy reverse proxy that serves the
#                               production build with the per-locale
#                               routing the production reverse proxy
#                               will use (per ADR-0019)
#
# Usage from infra/local/:
#   cp .env.example .env
#   $EDITOR .env  # set strong dev passwords
#   docker compose -f dev.compose.yml up -d
#
# To bring up viewers too:
#   docker compose -f dev.compose.yml --profile dbtools --profile observability up -d
#
# State persists in named volumes (`apf-portal-postgres-data`,
# `apf-portal-redis-data`). To wipe and start fresh:
#   docker compose -f dev.compose.yml down -v
#
# Bootstrap SQL (audit roles + schema, per ADR-0013) is applied on
# first boot from init/postgres/. To replay after editing it, wipe
# the volume with `down -v` above.

name: apf-portal-dev

# Shared base for the ADR-0030 `apps` profile — dev servers for the three
# Nx apps. The repo is bind-mounted for hot reload; node_modules and the
# Nx cache live in named volumes so the container's install (native
# modules built for THIS image) is never shadowed by the host's.
x-app-base: &app-base
  build:
    context: .
    dockerfile: Dockerfile.dev
  profiles: [apps]
  working_dir: /workspace
  volumes:
    - ../../:/workspace
    - app-node-modules:/workspace/node_modules
    - app-nx-cache:/workspace/.nx
  networks:
    - apf-portal-dev

services:
  postgres:
    image: postgres:17.2-alpine
    container_name: apf-portal-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-portal}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in infra/local/.env}
      POSTGRES_DB: ${POSTGRES_DB:-portal_dev}
    ports:
      - '${POSTGRES_PORT:-5432}:5432'
    volumes:
      - postgres-data:/var/lib/postgresql/data
      # Bootstrap SQL runs only on a fresh data volume.
      - ./init/postgres:/docker-entrypoint-initdb.d:ro
    networks:
      - apf-portal-dev
    healthcheck:
      test:
        ['CMD-SHELL', 'pg_isready -U "${POSTGRES_USER:-portal}" -d "${POSTGRES_DB:-portal_dev}"']
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7.4-alpine
    container_name: apf-portal-redis
    restart: unless-stopped
    # AOF (`--appendonly yes`) keeps sessions across container restarts —
    # the dev equivalent of the persistence we want from the prod
    # Sentinel cluster (per ADR-0010 §"Persistence").
    command:
      - redis-server
      - --requirepass
      - ${REDIS_PASSWORD:?REDIS_PASSWORD must be set in infra/local/.env}
      - --appendonly
      - 'yes'
    ports:
      - '${REDIS_PORT:-6379}:6379'
    volumes:
      - redis-data:/data
    networks:
      - apf-portal-dev
    healthcheck:
      test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'PING']
      interval: 5s
      timeout: 3s
      retries: 5

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.150.1
    container_name: apf-portal-otel-collector
    restart: unless-stopped
    command: ['--config=/etc/otel-collector.yaml']
    volumes:
      - ./otel-collector.yaml:/etc/otel-collector.yaml:ro
    ports:
      # OTLP receivers — the BFF and the SPA send here.
      - '${OTEL_GRPC_PORT:-4317}:4317'
      - '${OTEL_HTTP_PORT:-4318}:4318'
    networks:
      - apf-portal-dev

  # ------------------------------------------------------------------
  # Optional: Postgres GUI (`--profile dbtools`).
  # pgweb is a lightweight, no-config alternative to pgAdmin.
  # ------------------------------------------------------------------
  pgweb:
    image: sosedoff/pgweb:0.16.2
    container_name: apf-portal-pgweb
    restart: unless-stopped
    profiles: [dbtools]
    # Use discrete CLI flags rather than `PGWEB_DATABASE_URL`. The URL
    # form is fragile: any special character in POSTGRES_PASSWORD
    # (`@`, `#`, `:`, `/`, `?`, `%`, `&`, …) breaks the userinfo
    # parser, and the resulting "Invalid URL" error is opaque. Discrete
    # flags accept any password verbatim. The image's ENTRYPOINT
    # already passes `--bind=0.0.0.0 --listen=8081`; these args are
    # appended.
    command:
      - --host=postgres
      - --port=5432
      - --user=${POSTGRES_USER:-portal}
      - --pass=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in infra/local/.env}
      - --db=${POSTGRES_DB:-portal_dev}
      - --ssl=disable
    ports:
      - '${PGWEB_PORT:-8081}:8081'
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - apf-portal-dev

  # ------------------------------------------------------------------
  # Optional: Jaeger UI for trace exploration (`--profile observability`).
  # The collector pipeline above always tries to forward to `jaeger:4317`;
  # when this profile is off, those exports fail silently (logged at
  # warn level by the collector — no blocking effect on the debug
  # exporter that prints traces to stdout).
  # OTLP ports (4317/4318) are NOT published from this service to
  # avoid colliding with the collector — the collector talks to
  # Jaeger on the internal Compose network.
  #
  # Image is Jaeger v2 (`jaegertracing/jaeger`, distinct from the v1
  # `jaegertracing/all-in-one`). v2 is the rewrite built on top of
  # OpenTelemetry Collector and is the actively-developed line. OTLP
  # receivers are enabled by default (no COLLECTOR_OTLP_ENABLED env
  # needed) and the UI ships a built-in Light/Dark/Auto theme
  # selector — features the v1 image did not have.
  # ------------------------------------------------------------------
  jaeger:
    image: jaegertracing/jaeger:2.17.0
    container_name: apf-portal-jaeger
    restart: unless-stopped
    profiles: [observability]
    ports:
      - '${JAEGER_UI_PORT:-16686}:16686'
    networks:
      - apf-portal-dev

  # ------------------------------------------------------------------
  # Optional: Caddy reverse proxy for the production build
  # (`--profile serve-static`).
  #
  # Workflow:
  #   pnpm exec nx build portal-shell --configuration=production
  #   ./infra/local/dev.sh up serve-static
  #   open http://localhost:${SERVE_STATIC_PORT:-4200}/
  #
  # Mirrors what the production reverse proxy will do per ADR-0019:
  # smart redirect at `/` (Accept-Language → `/fr/` or `/en/`),
  # per-locale SPA fallback, and `/{locale}/<deep-link>` routing.
  # No locking-down on TLS / auth — this is a local convenience that
  # boots in <1 s and binds to localhost only.
  # ------------------------------------------------------------------
  serve-static:
    image: caddy:2.10-alpine
    container_name: apf-portal-serve-static
    restart: unless-stopped
    profiles: [serve-static]
    command: ['caddy', 'run', '--config', '/etc/caddy/Caddyfile', '--adapter', 'caddyfile']
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      # Bind-mount the production build folder read-only. The path
      # resolves relative to this compose file, i.e.
      # <repo>/dist/apps/portal-shell/browser/. Empty until the
      # production build runs — caddy will 404 every request in that
      # state, which is the right signal.
      - ../../dist/apps/portal-shell/browser:/srv/dist:ro
    ports:
      - '${SERVE_STATIC_PORT:-4200}:4200'
    networks:
      - apf-portal-dev

  # ------------------------------------------------------------------
  # ADR-0030 dockerised full-stack dev mode (`--profile apps`).
  #
  #   ./infra/local/dev.sh up apps   → infra + the three dev servers,
  #                                    no native Node/pnpm on the host.
  #
  # Hot reload via the repo bind-mount. See docs/setup/ for the
  # "which mode when" guidance (native / devcontainer / compose apps).
  # NOTE: the SPA dev servers default to 4200/4300 — the same 4200 the
  # `serve-static` profile uses; don't run `apps` and `serve-static`
  # together, or override SHELL_PORT.
  # ------------------------------------------------------------------

  # One-shot: populate the shared node_modules volume once so the three
  # app services don't race three concurrent installs on it. Exits when
  # done; the apps gate on its successful completion.
  apps-deps:
    <<: *app-base
    container_name: apf-portal-apps-deps
    command: ['pnpm', 'install', '--frozen-lockfile']

  portal-bff:
    <<: *app-base
    container_name: apf-portal-bff-dev
    environment:
      APF_ROLE: bff
      NODE_ENV: development
      PORT: '3000'
      # Host-specific URLs rebuilt from infra/local/.env creds → Compose
      # service names. Compose `environment` wins over `env_file`, so
      # these override the localhost values in the BFF's own .env.
      DATABASE_URL: 'postgresql://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-portal_dev}?schema=public'
      REDIS_URL: 'redis://default:${REDIS_PASSWORD}@redis:6379/0'
      OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel-collector:4318/v1/traces'
      # OIDC redirect URIs. Driven by infra/local/.env so switching
      # between the two access modes — `localhost` (default; VSCode
      # Remote-SSH port-forwarding) and `https` hostname (the
      # ADR-0030 mkcert path) — is a single-file change. Unset →
      # localhost defaults; set in infra/local/.env to override with
      # an `https://apf-portal.dev-XX.local:…` hostname. See the
      # "Switching between dev modes" subsection of infra/README.md.
      # Compose `environment` overrides `env_file`, so the BFF's
      # own .env stays at localhost (so native `nx serve` works
      # untouched) and only `infra/local/.env` carries the
      # docker-apps-mode override.
      ENTRA_REDIRECT_URI: ${ENTRA_REDIRECT_URI:-http://localhost:3000/api/auth/callback}
      ENTRA_POST_LOGOUT_REDIRECT_URI: ${ENTRA_POST_LOGOUT_REDIRECT_URI:-http://localhost:4200/}
      ENTRA_ADMIN_REDIRECT_URI: ${ENTRA_ADMIN_REDIRECT_URI:-http://localhost:3000/api/admin/auth/callback}
      ENTRA_ADMIN_POST_LOGOUT_REDIRECT_URI: ${ENTRA_ADMIN_POST_LOGOUT_REDIRECT_URI:-http://localhost:4300/}
    # Secrets (Entra / session / jwks) come from the BFF's own dev env.
    # `required: false` so SPA-only devs can `up` without it — the BFF
    # then fails its own boot-time config validators with a clear message
    # rather than Compose erroring on a missing file.
    env_file:
      - path: ../../apps/portal-bff/.env
        required: false
    command: ['pnpm', 'exec', 'nx', 'serve', 'portal-bff']
    ports:
      - '${BFF_PORT:-3000}:3000'
    depends_on:
      apps-deps:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  portal-shell:
    <<: *app-base
    container_name: apf-portal-shell-dev
    environment:
      # Read by apps/portal-shell/proxy.conf.js — points the dev-server
      # /api proxy at the BFF container by name (Compose DNS). Native
      # `nx serve` leaves BFF_TARGET unset and falls back to localhost.
      BFF_TARGET: http://portal-bff:3000
    # `--configuration=${NX_SERVE_CONFIGURATION:-development}` is
    # interpolated by Compose at YAML parse time from
    # `infra/local/.env`. Set `NX_SERVE_CONFIGURATION=https` there to
    # serve over TLS — required when accessing via a hostname (Entra
    # rejects `http:` for non-`localhost` redirect URIs). See the
    # `https` configuration in apps/portal-shell/project.json + the
    # "HTTPS dev-server setup" section in infra/README.md for the
    # mkcert procedure.
    command:
      [
        'pnpm',
        'exec',
        'nx',
        'serve',
        'portal-shell',
        '--host',
        '0.0.0.0',
        '--port',
        '4200',
        '--configuration=${NX_SERVE_CONFIGURATION:-development}',
      ]
    ports:
      - '${SHELL_PORT:-4200}:4200'
    depends_on:
      apps-deps:
        condition: service_completed_successfully

  portal-admin:
    <<: *app-base
    container_name: apf-portal-admin-dev
    environment:
      # See portal-shell — same proxy target for the admin SPA.
      BFF_TARGET: http://portal-bff:3000
    # See portal-shell for the NX_SERVE_CONFIGURATION rationale.
    command:
      [
        'pnpm',
        'exec',
        'nx',
        'serve',
        'portal-admin',
        '--host',
        '0.0.0.0',
        '--port',
        '4300',
        '--configuration=${NX_SERVE_CONFIGURATION:-development}',
      ]
    ports:
      - '${ADMIN_PORT:-4300}:4300'
    depends_on:
      apps-deps:
        condition: service_completed_successfully

volumes:
  postgres-data:
    name: apf-portal-postgres-data
  redis-data:
    name: apf-portal-redis-data
  app-node-modules:
    name: apf-portal-app-node-modules
  app-nx-cache:
    name: apf-portal-app-nx-cache

networks:
  apf-portal-dev:
    name: apf-portal-dev
