#!/usr/bin/env bash
# setup-runner-tls.sh
#
# One-time host setup for per-runner TLS via path-routing on
# mcp.buildmymcpserver.com. Run as root on the BMM host AFTER you've done
# the prereqs below. Idempotent — safe to re-run.
#
# Why path-routing instead of per-subdomain: Cloudflare's free Universal
# SSL only covers ONE-level wildcards (*.buildmymcpserver.com). A
# two-level wildcard (*.mcp.buildmymcpserver.com) needs CF Advanced
# Certificate Manager ($10/mo) OR a Let's Encrypt wildcard via certbot
# + DNS-01. To keep recurring cost at zero, we serve every runner under
# the single subdomain mcp.buildmymcpserver.com — which IS covered by
# Universal SSL — and route by path: https://mcp../<slug>/mcp.
#
# What it does:
#   1. Creates /opt/buildmymcpserver/runner-map/ (volume-mounted into bmm-api
#      and bmm-generator — they drop one .conf fragment per live runner)
#   2. Installs an nginx vhost that catches *.mcp.buildmymcpserver.com,
#      reads slug→port from a combined map file, and reverse-proxies to the
#      runner on localhost
#   3. Installs a systemd service that inotify-watches the map dir, combines
#      all fragments into a single map file, and reloads nginx on any change
#
# After this script:
#   - In docker-compose.prod.yml, add a volume mount to BOTH api and generator:
#       volumes:
#         - /opt/buildmymcpserver/runner-map:/var/runner-map
#   - In .env.production, add:
#       MCP_DOMAIN=mcp.buildmymcpserver.com
#   - docker compose up -d --force-recreate api generator
#   - From now on every deployed runner gets https://<slug>.mcp.buildmymcpserver.com
#
# ─── PREREQS (do these in Cloudflare dashboard before running) ───────────
#   A. DNS:  Add an A-record  'mcp.buildmymcpserver.com'  →  213.239.213.217
#            Proxy status:    Proxied (orange cloud)
#            (The wildcard *.mcp record from earlier setup is no longer
#            needed for path-routing — safe to leave or delete.)
#   B. SSL:  Cloudflare → SSL/TLS → Origin Server → Create Certificate
#            (likely already done — the same Origin cert from the prior
#            setup attempt covers mcp.buildmymcpserver.com too, since
#            you generated it with all four hostnames in the SAN list).
#            Path:  /etc/ssl/buildmymcpserver/mcp-runners.crt  (644)
#                   /etc/ssl/buildmymcpserver/mcp-runners.key  (600 root:root)
#   C. SSL mode: Cloudflare → SSL/TLS → Overview → "Full (strict)"
#
# Run:    sudo bash scripts/setup-runner-tls.sh
set -euo pipefail

if [[ "${EUID}" -ne 0 ]]; then
  echo "Run as root (sudo bash $0)."
  exit 1
fi

MAP_DIR="/opt/buildmymcpserver/runner-map"
COMBINED="/opt/buildmymcpserver/runner-map.combined"
VHOST_DST="/etc/nginx/sites-available/bmm-mcp-runners"
VHOST_LNK="/etc/nginx/sites-enabled/bmm-mcp-runners"
CERT="/etc/ssl/buildmymcpserver/mcp-runners.crt"
KEY="/etc/ssl/buildmymcpserver/mcp-runners.key"

echo "─── checking prereqs ───────────────────────────────────────"
for f in "$CERT" "$KEY"; do
  if [[ ! -f "$f" ]]; then
    echo "MISSING: $f — see PREREQS at the top of this script."
    exit 1
  fi
done

if ! command -v inotifywait >/dev/null; then
  echo "Installing inotify-tools…"
  apt-get update -qq
  apt-get install -y -qq inotify-tools
fi

echo "─── creating map dir + initial combined file ──────────────"
mkdir -p "$MAP_DIR"
chmod 755 "$MAP_DIR"
touch "$COMBINED"
chmod 644 "$COMBINED"

echo "─── writing nginx vhost ──────────────────────────────────"
cat > "$VHOST_DST" <<'NGINX'
# BMM per-runner proxy via PATH routing on mcp.buildmymcpserver.com.
# The combined snippet file (regenerated by the bmm-api and bmm-generator
# containers + reloaded by the systemd inotify watcher) is a sequence of
# `if ($bmm_slug = "<slug>") { set $bmm_port <port>; }` lines that map the
# slug captured from the URL path to the local runner port.

server {
  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name mcp.buildmymcpserver.com;

  ssl_certificate     /etc/ssl/buildmymcpserver/mcp-runners.crt;
  ssl_certificate_key /etc/ssl/buildmymcpserver/mcp-runners.key;

  client_max_body_size 4M;

  # Cheap health probe for monitoring — doesn't go through the slug router.
  location = /health {
    return 200 "ok\n";
    add_header Content-Type text/plain;
  }

  # RFC 9728 for path-routed resources:
  # /<slug>/mcp derives metadata at /.well-known/oauth-protected-resource/<slug>/mcp.
  # Route that well-known URL back to the same runner while preserving only the
  # resource sub-path after the slug.
  location ~ ^/\.well-known/oauth-protected-resource/(?<bmm_slug>[a-z0-9][a-z0-9-]*)(?<bmm_path>/.*)?$ {
    set $bmm_port "";
    include /opt/buildmymcpserver/runner-map.combined;

    if ($bmm_port = "") {
      return 404;
    }

    proxy_pass http://127.0.0.1:$bmm_port/.well-known/oauth-protected-resource$bmm_path;
    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 https;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 600s;
  }

  # /<slug>/<rest> → 127.0.0.1:<port>/<rest>
  location ~ ^/(?<bmm_slug>[a-z0-9][a-z0-9-]*)(?<bmm_path>/.*)?$ {
    set $bmm_port "";
    include /opt/buildmymcpserver/runner-map.combined;

    # Unknown slug — return 404 instead of a confusing default.
    if ($bmm_port = "") {
      return 404;
    }

    # Default sub-path to /  when client asked for /<slug> with no trailing slash.
    set $bmm_target_path $bmm_path;
    if ($bmm_target_path = "") {
      set $bmm_target_path "/";
    }

    proxy_pass http://127.0.0.1:$bmm_port$bmm_target_path;
    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 https;
    # MCP uses Streamable HTTP — disable buffering so response chunks flow.
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 600s;
  }

  # Root of mcp.buildmymcpserver.com — nothing to serve here.
  location = / {
    return 404;
  }
}
NGINX
ln -sf "$VHOST_DST" "$VHOST_LNK"

echo "─── writing systemd watcher service ──────────────────────"
cat > /etc/systemd/system/bmm-runner-map.service <<EOF
[Unit]
Description=BMM runner-map combiner + nginx reload
After=network.target

[Service]
Type=simple
ExecStartPre=/bin/bash -c 'cat ${MAP_DIR}/*.conf 2>/dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload || true'
ExecStart=/bin/bash -c 'while inotifywait -q -e create,modify,delete,moved_to,moved_from ${MAP_DIR}; do cat ${MAP_DIR}/*.conf 2>/dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload; done'
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now bmm-runner-map

echo "─── verifying nginx config + reload ──────────────────────"
nginx -t
nginx -s reload || true

echo ""
echo "─── DONE ─────────────────────────────────────────────────"
echo ""
echo "Next steps (one-time):"
echo ""
echo "1) Edit /opt/buildmymcpserver/docker-compose.prod.yml — add to BOTH"
echo "   the 'api' and 'generator' services:"
echo ""
echo "     volumes:"
echo "       - /opt/buildmymcpserver/runner-map:/var/runner-map"
echo ""
echo "2) Edit /opt/buildmymcpserver/.env.production — add:"
echo ""
echo "     MCP_DOMAIN=mcp.buildmymcpserver.com"
echo ""
echo "3) Restart api + generator so they pick up the env + volume:"
echo ""
echo "     cd /opt/buildmymcpserver"
echo "     docker compose --env-file .env.production -f docker-compose.prod.yml \\"
echo "       up -d --force-recreate api generator"
echo ""
echo "Test (after at least one runner has been deployed):"
echo "  curl -I https://<slug>.mcp.buildmymcpserver.com/health"
echo ""
echo "If you ever need to verify the map state:"
echo "  cat ${COMBINED}"
echo "  systemctl status bmm-runner-map"
