services:
  # Note: MariaDB is external service. You can find more information about the configuration here:
  # https://hub.docker.com/_/mariadb
  db:
    # Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
    image: docker.io/library/mariadb:11.8.8-noble@sha256:be1ef4fe5f14589325c08a41c76334097ce66c86264b75e1d28342c742782a61
    restart: always
    command: --transaction-isolation=READ-COMMITTED
    volumes:
      - db:/var/lib/mysql
    ports:
      - 127.0.0.1:3306:3306
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_PASSWORD=password
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud

  # Note: Redis is an external service. You can find more information about the configuration here:
  # https://hub.docker.com/_/redis
  redis:
    image: docker.io/library/redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
    restart: always

  app:
    image: ${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud:32.0.11@sha256:3ae4045d53e890680ec35e3e4349332123d959c72808aceb44930e79f1be9b9b}
    restart: always
    ports:
      - 127.0.0.1:8080:80
    depends_on:
      - redis
      - db
    volumes:
      - nextcloud:/var/www/html
      - ./app-hooks:/docker-entrypoint-hooks.d:ro
      # Mount OIDC development directory outside /var/www/html to avoid rsync conflicts
      # The post-installation hook will register /opt/apps as an additional app directory
      #- ./third_party:/opt/apps:ro
      #- ./third_party/astrolabe:/opt/apps/astrolabe:ro
      #- ./third_party/oidc:/opt/apps/oidc:ro
    environment:
      - NEXTCLOUD_TRUSTED_DOMAINS=app
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=admin
      - MYSQL_PASSWORD=password
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_HOST=db
      - REDIS_HOST=redis
      # Set MCP_SERVER_URL to enable Astrolabe OAuth client auto-creation
      # (before-starting hook creates OIDC client + stores credentials in config.php)
      # Example: MCP_SERVER_URL=http://mcp-login-flow:8004 docker compose --profile login-flow up -d
      - MCP_SERVER_URL=${MCP_SERVER_URL:-}
    healthcheck:
      test: ["CMD-SHELL", "curl -Ss http://localhost/status.php | grep '\"installed\":true' || exit 1"]
      interval: 10s
      timeout: 30s
      retries: 30

  recipes:
    image: docker.io/library/nginx:1.31.1-alpine@sha256:8b1e78743a03dbb2c95171cc58639fef29abc8816598e27fb910ed2e621e589a
    restart: always
    volumes:
      - ./tests/fixtures/test_recipe.html:/usr/share/nginx/html/test_recipe.html:ro
      - ./tests/fixtures/nginx.conf:/etc/nginx/nginx.conf:ro

  unstructured:
    image: downloads.unstructured.io/unstructured-io/unstructured-api:0.1.7@sha256:ee89a87a545b6a7d56609607aced71b5b1acf0211400bf2d11a8f70f209a93bd
    restart: always
    ports:
      - 127.0.0.1:8005:8000
    # Unstructured API runs on port 8000 internally
    # We expose it on 8005 externally to avoid conflict
    profiles:
      - unstructured

  mcp:
    build: .
    restart: always
    command: ["--transport", "streamable-http"]
    depends_on:
      app:
        condition: service_healthy
    ports:
      - 127.0.0.1:8000:8000
      - 127.0.0.1:9090:9090
    volumes:
      - mcp-data:/app/data
      - ./settings.toml.example:/app/settings.toml:ro
    environment:
      - NEXTCLOUD_HOST=http://app:80
      - NEXTCLOUD_USERNAME=admin
      - NEXTCLOUD_PASSWORD=admin
      - NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080

      # Pin the token DB inside the mcp-data volume so the runtime default
      # (ephemeral tempfile) doesn't silently apply inside containers.
      - TOKEN_STORAGE_DB=/app/data/tokens.db

      # Semantic search configuration (ADR-007, ADR-021)
      - ENABLE_SEMANTIC_SEARCH=true
      - VECTOR_SYNC_SCAN_INTERVAL=5
      - VECTOR_SYNC_PROCESSOR_WORKERS=2
      # Required to enable the /webhooks/nextcloud receiver (GHSA-8vh3-g2qg-2h2c).
      # Without it the route is not mounted and vector sync relies on polling.
      - WEBHOOK_SECRET=${WEBHOOK_SECRET:-dev-webhook-secret-change-me}

      #- LOG_FORMAT=json

      # Qdrant configuration (three modes):
      # 1. Network mode: Set QDRANT_URL=http://qdrant:6333 (requires qdrant service)
      # 2. In-memory mode: Set QDRANT_LOCATION=:memory: (default if nothing set)
      # 3. Persistent local: Set QDRANT_LOCATION=/app/data/qdrant (stored in mcp-data volume)
      - QDRANT_LOCATION=":memory:"
      #- QDRANT_URL=http://qdrant:6333  # Uncomment for network mode
      #- QDRANT_API_KEY=${QDRANT_API_KEY:-my_secret_api_key}  # Only for network mode

      # Observability
      #- OTEL_SERVICE_NAME=nextcloud-mcp-docker-compose
      #- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

      # Collection naming: Auto-generated as {deployment-id}-{model-name}
      # - Deployment ID: OTEL_SERVICE_NAME (if set) or hostname (fallback)
      # - Model name: OLLAMA_EMBEDDING_MODEL
      # - Example: "nextcloud-mcp-server-nomic-embed-text"
      # - Changing models creates new collection (requires re-embedding)
      # - Set QDRANT_COLLECTION to override auto-generation:
      #- QDRANT_COLLECTION=nextcloud_content

      # Ollama configuration (optional - uses SimpleEmbeddingProvider if not set)
      # - OLLAMA_BASE_URL=http://ollama:11434
      # - OLLAMA_EMBEDDING_MODEL=nomic-embed-text  # Changing this creates new collection
      # - OLLAMA_VERIFY_SSL=false

      # Document chunking configuration (for vector embeddings)
      # Tune these based on your embedding model and content type
      # - DOCUMENT_CHUNK_SIZE=512      # Words per chunk (default: 512)
      # - DOCUMENT_CHUNK_OVERLAP=50    # Overlapping words (default: 50, recommended: 10-20% of chunk size)
    profiles:
      - single-user

  mcp-multi-user-basic:
    build: .
    restart: always
    command: ["--transport", "streamable-http"]
    depends_on:
      app:
        condition: service_healthy
    ports:
      - 127.0.0.1:8003:8000
    environment:
      # Multi-user BasicAuth pass-through mode (ADR-020, ADR-022)
      - NEXTCLOUD_HOST=http://app:80
      - NEXTCLOUD_MCP_SERVER_URL=http://localhost:8003
      - NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
      - MCP_DEPLOYMENT_MODE=multi_user_basic
      - ENABLE_BACKGROUND_OPERATIONS=true

      # Token storage (required for middleware initialization).
      # Source the key from .env — see env.sample. To generate a fresh key:
      #   python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
      - TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:?TOKEN_ENCRYPTION_KEY must be set in .env (see env.sample)}
      - TOKEN_STORAGE_DB=/app/data/tokens.db

      - ENABLE_SEMANTIC_SEARCH=true
      # Required to enable the /webhooks/nextcloud receiver (GHSA-8vh3-g2qg-2h2c).
      # Without it the route is not mounted and vector sync relies on polling.
      - WEBHOOK_SECRET=${WEBHOOK_SECRET:-dev-webhook-secret-change-me}
      # Tuned cadence for the multi-user background-sync integration suite,
      # which provisions a user, creates a note, then waits ~90s for it to be
      # indexed. Two independent knobs matter here:
      #   * USER_POLL_INTERVAL gates how fast the background-sync user manager
      #     *discovers* a freshly-provisioned user (its scanner runs an initial
      #     scan immediately on start). The 60s default left too little of the
      #     90s budget, so drop it to 5s for prompt discovery.
      #   * SCAN_INTERVAL gates *re-scan* churn. Each scan re-queues the user's
      #     whole corpus, so a very short interval (e.g. 5s) floods the single
      #     processor worker faster than it drains on slower CI runners
      #     (pending climbs, indexed stays 0). Keep it moderate: the immediate
      #     initial scan already indexes the corpus once; 30s re-scans avoid
      #     the flood while still catching a note created just after that scan.
      - VECTOR_SYNC_USER_POLL_INTERVAL=5
      - VECTOR_SYNC_SCAN_INTERVAL=30
      - VECTOR_SYNC_PROCESSOR_WORKERS=1

      # OAuth client for the MCP server's IdP registration. Falls back to DCR
      # if unset — but with Nextcloud's built-in `oidc` app, DCR clients expire
      # after ~1h (client_expire_time) and break auth permanently (see #907).
      # For any non-CI deployment, register a static client and set these:
      # - NEXTCLOUD_OIDC_CLIENT_ID=your_client_id
      # - NEXTCLOUD_OIDC_CLIENT_SECRET=your_client_secret

      # Management API allowlist (ADR-018): only Astrolabe-issued tokens may
      # call /api/management/*. Default test client_id used by the
      # configure_astrolabe_for_mcp_server fixture.
      - ALLOWED_MGMT_CLIENT=nextcloudMcpServerUIPublicClient

      # NO admin credentials - credentials come from client Authorization header
    volumes:
      - multi-user-basic-data:/app/data
      - ./settings.toml.example:/app/settings.toml:ro
    profiles:
      - multi-user-basic

  keycloak:
    image: quay.io/keycloak/keycloak:26.6.3@sha256:5fdbf2dbb5897cc34e82de49d13e23db011f9925089dbc555fc095f2c8bc1dac
    command:
      - "start-dev"
      - "--import-realm"
      - "--hostname=http://localhost:8888"
      - "--hostname-strict=false"
      - "--hostname-backchannel-dynamic=true"
      - "--features=preview"  # Enable Legacy V1 token exchange (supports both Standard V2 and Legacy V1)
    ports:
      - 127.0.0.1:8888:8080
    environment:
      - KC_BOOTSTRAP_ADMIN_USERNAME=admin
      - KC_BOOTSTRAP_ADMIN_PASSWORD=admin
    volumes:
      - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro
    healthcheck:
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /realms/nextcloud-mcp HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q 'HTTP/1.1 200'"]
      interval: 10s
      timeout: 5s
      retries: 30
    profiles:
      - keycloak

  mcp-keycloak:
    build: .
    command: ["--transport", "streamable-http", "--oauth", "--port", "8002"]
    restart: always
    depends_on:
      keycloak:
        condition: service_healthy
      app:
        condition: service_started
    ports:
      - 127.0.0.1:8002:8002
    environment:
      # Generic OIDC configuration (external IdP mode - Keycloak)
      # Provider auto-detected from OIDC_DISCOVERY_URL issuer
      # Using internal Docker hostname for discovery to get consistent issuer
      - OIDC_DISCOVERY_URL=http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
      - NEXTCLOUD_OIDC_CLIENT_ID=nextcloud-mcp-server
      - NEXTCLOUD_OIDC_CLIENT_SECRET=mcp-secret-change-in-production
      - OIDC_JWKS_URI=http://keycloak:8080/realms/nextcloud-mcp/protocol/openid-connect/certs

      # Nextcloud API endpoint (for accessing APIs with validated token)
      - NEXTCLOUD_HOST=http://app:80
      - NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
      - NEXTCLOUD_RESOURCE_URI=nextcloud  # ADR-005: Keycloak uses client IDs as audiences, not URLs
      - NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8888/realms/nextcloud-mcp

      # Refresh token storage (ADR-002 Tier 1 & 2). Source from .env.
      - ENABLE_BACKGROUND_OPERATIONS=true
      - TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:?TOKEN_ENCRYPTION_KEY must be set in .env (see env.sample)}
      - TOKEN_STORAGE_DB=/app/data/tokens.db

      # Login Flow v2 (ADR-022) with external IdP — derived from the
      # auto-detected LOGIN_FLOW deployment mode; no separate flag needed.
      - ENABLE_DCR=true

      # OAuth scopes (optional - uses defaults if not specified)
      - NEXTCLOUD_OIDC_SCOPES=openid profile email offline_access notes.read notes.write calendar.read calendar.write contacts.read contacts.write cookbook.read cookbook.write deck.read deck.write tables.read tables.write files.read files.write sharing.read sharing.write todo.read todo.write

      # NO admin credentials - using external IdP OAuth only!
    volumes:
      - keycloak-tokens:/app/data
      - keycloak-oauth-storage:/app/.oauth
      - ./settings.toml.example:/app/settings.toml:ro
    profiles:
      - keycloak

  # Login Flow v2 mode (ADR-022)
  # Test with: docker compose --profile login-flow up --build -d
  mcp-login-flow:
    build: .
    restart: always
    # --oauth enables the OAuth/OIDC identity layer that Login Flow v2 builds on
    # (user identity via OAuth session, Nextcloud access via app passwords)
    command: ["--transport", "streamable-http", "--oauth", "--port", "8004"]
    depends_on:
      app:
        condition: service_healthy
    ports:
      - 127.0.0.1:8004:8004
    environment:
      - NEXTCLOUD_HOST=http://app:80
      - NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004
      #- NEXTCLOUD_MCP_SERVER_URL=https://nextcloud-mcp-dev.tail148d5.ts.net
      - NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080

      # Login Flow v2 (ADR-022) — the deployment mode is the single switch;
      # the browser-based app-password layer is derived automatically.
      - MCP_DEPLOYMENT_MODE=login_flow

      # NOTE: this dev/test service relies on Dynamic Client Registration for
      # the MCP server's own OIDC client, which is fine for short-lived CI runs.
      # For a real self-hosted login_flow deployment, register a STATIC client
      # in Nextcloud (Administration → OpenID Connect provider) and set
      # NEXTCLOUD_OIDC_CLIENT_ID / NEXTCLOUD_OIDC_CLIENT_SECRET — the built-in
      # `oidc` app deletes DCR clients after ~1h, which breaks auth (see #907
      # and docs/login-flow-v2.md → Troubleshooting).

      # Token storage (required for app password + session persistence).
      # Source the key from .env — see env.sample. To generate a fresh key:
      #   python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
      - TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:?TOKEN_ENCRYPTION_KEY must be set in .env (see env.sample)}
      - TOKEN_STORAGE_DB=/app/data/tokens.db

      # Semantic search
      - ENABLE_SEMANTIC_SEARCH=true
      - VECTOR_SYNC_SCAN_INTERVAL=60
      - VECTOR_SYNC_PROCESSOR_WORKERS=1
      # Required to enable the /webhooks/nextcloud receiver (GHSA-8vh3-g2qg-2h2c).
      # Without it the route is not mounted and vector sync relies on polling.
      - WEBHOOK_SECRET=${WEBHOOK_SECRET:-dev-webhook-secret-change-me}

      # Management API allowlist (ADR-018) — matches mcp-multi-user-basic so
      # the same configure_astrolabe_for_mcp_server fixture (which creates the
      # static `nextcloudMcpServerUIPublicClient` OIDC client) works for tests
      # against this profile too.
      - ALLOWED_MGMT_CLIENT=astrolabeMcpClientOAuth00000000000
    volumes:
      - login-flow-data:/app/data
      - login-flow-oauth-storage:/app/.oauth
      - ./settings.toml.example:/app/settings.toml:ro
    profiles:
      - login-flow

  # Tailscale Funnel for Claude AI connector testing
  # Usage: docker compose --profile login-flow --profile claude-funnel up --build -d
  # Requires: TS_AUTHKEY in .env file (see env.sample.claude-funnel)
  tailscale-mcp:
    image: docker.io/tailscale/tailscale:v1.98.4@sha256:25cde9ad76020b0e29229136d0c38b5962e9a0e1774ffac9b0df68e4a37d6cf0
    hostname: nextcloud-mcp-dev
    restart: always
    environment:
      - TS_AUTHKEY=${TS_AUTHKEY}
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_SERVE_CONFIG=/config/serve-config.json
      - TS_HOSTNAME=nextcloud-mcp-dev
      - TS_EXTRA_ARGS=--advertise-tags=tag:container
    volumes:
      - tailscale-state:/var/lib/tailscale
      - ./claude-funnel/serve-config.json:/config/serve-config.json:ro
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    profiles:
      - claude-funnel

  nginx-claude-filter:
    image: docker.io/library/nginx:1.31.1-alpine@sha256:8b1e78743a03dbb2c95171cc58639fef29abc8816598e27fb910ed2e621e589a
    restart: always
    depends_on:
      - mcp-login-flow
    volumes:
      - ./claude-funnel/nginx.conf:/etc/nginx/nginx.conf:ro
    profiles:
      - claude-funnel

  qdrant:
    image: docker.io/qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c
    restart: always
    ports:
      - 127.0.0.1:6333:6333  # REST API
      - 127.0.0.1:6334:6334  # gRPC (optional)
    volumes:
      - qdrant-data:/qdrant/storage
    environment:
      - QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:-my_secret_api_key}
    healthcheck:
      test: ["CMD-SHELL", "test -f /qdrant/.qdrant-initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
    profiles:
      - qdrant

  # Centralized Postgres backend for token storage. Used by:
  #   1. Integration tests gated on @pytest.mark.postgres — they read
  #      TEST_DATABASE_URL=postgresql+asyncpg://mcp:mcp@localhost:5433/mcp.
  #   2. Manual smoke testing of the HA (multi-replica) deployment story.
  # Bring up with:  docker compose --profile postgres up -d postgres-test
  # See ADR-026 for the pluggable-database-backend design.
  postgres-test:
    image: docker.io/library/postgres:16-alpine@sha256:16bc17c64a573ef34162af9298258d1aec548232985b33ed7b1eac33ba35c229
    restart: unless-stopped
    environment:
      POSTGRES_USER: mcp
      POSTGRES_PASSWORD: mcp
      POSTGRES_DB: mcp
    ports:
      - 127.0.0.1:5433:5432
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mcp -d mcp"]
      interval: 5s
      timeout: 3s
      retries: 10
    profiles:
      - postgres

volumes:
  nextcloud:
  db:
  keycloak-tokens:
  keycloak-oauth-storage:
  login-flow-data:
  login-flow-oauth-storage:
  qdrant-data:
  mcp-data:
  multi-user-basic-data:
  tailscale-state:
