#!/bin/bash
# Officer — Docker Compose service setup
# Sets up Docker Compose services (Nginx Proxy Manager, PostgreSQL, Mailhog, Redis, SearXNG).
# Outputs parseable key=value lines to stdout; all prompts go to stderr.
#
# Usage:
#   bash scripts/setup/setup-dockers.sh
#   eval "$(bash scripts/setup/setup-dockers.sh)"
#
# Environment overrides:
#   SETUP_DOCKER_SERVICES="1 2 3"  — pre-select services (or "all"/"none")
#   SETUP_DOCKER_NETWORK="services" — docker network name
#   SETUP_NPM_BIND="100.64.0.8"     — host address Nginx Proxy Manager publishes on. Defaults to this
#                                     node's Tailscale IPv4; set it explicitly to bind somewhere else.

set -e

# Resolve the real user's home even when running under sudo
if [[ -n "${SUDO_USER:-}" ]]; then
  REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
else
  REAL_HOME="$HOME"
fi

GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

info()  { echo -e "${CYAN}::${NC} $*" >&2; }
ok()    { echo -e "  ${GREEN}✓${NC} $1" >&2; }
warn()  { echo -e "  ${YELLOW}!${NC} $1" >&2; }
fail()  { echo -e "  ${RED}✗${NC} $1" >&2; }

prompt_value() {
  local varname="$1" prompt="$2" default="$3"
  if [[ -n "$default" ]]; then
    echo -en "  ${BOLD}${prompt}${NC} [${default}]: " >&2
  else
    echo -en "  ${BOLD}${prompt}${NC}: " >&2
  fi
  read -r val
  if [[ -z "$val" ]]; then val="$default"; fi
  eval "$varname=\"\$val\""
}

# ─── docker network ─────────────────────────────────────────────────────────
DOCKER_NETWORK="${SETUP_DOCKER_NETWORK:-services}"

# ─── Nginx Proxy Manager bind address ───────────────────────────────────────
#
# NPM is the only service here that ever published on 0.0.0.0, and a published Docker port is not
# behind the firewall: Docker writes its DNAT rules directly into the nat table, which UFW's INPUT
# chain never sees. `ufw default deny incoming` does not cover 80/443/81 — that is what the host's
# ufw-docker-rules.conf exists to patch, and patching a rule is weaker than never opening the socket.
#
# So bind to the tailnet address instead. The kernel then refuses the socket on every other interface
# and the firewall stops being load-bearing for this. The address is read at run time rather than
# passed in, because by the time this script runs the host provisioning has already done `tailscale up`.
resolve_npm_bind() {
  if [[ -n "${SETUP_NPM_BIND:-}" ]]; then
    echo "$SETUP_NPM_BIND"
    return
  fi
  local ip
  ip=$(tailscale ip -4 2>/dev/null | head -1)
  # 100.64.0.0/10 — the CGNAT range both Tailscale and Headscale allocate from. Anything outside it
  # means `tailscale ip` answered with something unexpected, and a bind address is not a value to
  # guess at: the whole point is that it is NOT reachable from the internet.
  if [[ "$ip" =~ ^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\. ]]; then
    echo "$ip"
    return
  fi
  echo ""
}

NPM_BIND="$(resolve_npm_bind)"

# Binding to an address that belongs to another service's interface makes that service a boot-order
# dependency: if tailscaled has not brought tailscale0 up yet, the container cannot get its socket and
# Docker falls back on the restart policy to retry. That converges, but only if the tailnet comes up
# at all on its own.
if [[ -n "$NPM_BIND" ]] && ! systemctl is-enabled --quiet tailscaled 2>/dev/null; then
  warn "tailscaled is not enabled at boot — NPM binds $NPM_BIND, which will not exist after a reboot"
  warn "until the tailnet is up. Fix with: sudo systemctl enable tailscaled"
fi

# Ensure network exists
if ! docker network inspect "$DOCKER_NETWORK" &>/dev/null; then
  docker network create "$DOCKER_NETWORK" >/dev/null 2>&1
  ok "Created docker network '$DOCKER_NETWORK'"
else
  ok "Docker network '$DOCKER_NETWORK' exists"
fi

# ─── service selection ───────────────────────────────────────────────────────
SERVICES="${SETUP_DOCKER_SERVICES:-}"

if [[ "$SERVICES" == "none" ]]; then
  info "Skipping Docker Compose services (SETUP_DOCKER_SERVICES=none)"
  exit 0
fi

if [[ -z "$SERVICES" ]]; then
  info "Select Docker Compose services to include:"
  echo "  [1] Nginx Proxy Manager (jc21/nginx-proxy-manager:latest)" >&2
  echo "  [2] PostgreSQL 18 (postgres:18-alpine)" >&2
  echo "  [3] Mailhog (mailhog/mailhog:latest)" >&2
  echo "  [4] Redis (redis:alpine)" >&2
  echo "  [5] SearXNG (searxng/searxng:latest)" >&2

  prompt_value SERVICES "Enter numbers separated by spaces (e.g. '1 2 4'), or 'all'" "all"
fi

if [[ "$SERVICES" == "all" ]]; then
  SERVICES="1 2 3 4 5"
fi

# ─── compose directory ──────────────────────────────────────────────────────
prompt_value COMPOSE_DIR "Docker Compose directory" "$REAL_HOME/.local/dockers"
mkdir -p "$COMPOSE_DIR"

# ─── build compose file ─────────────────────────────────────────────────────
cat > "$COMPOSE_DIR/docker-compose.yaml" <<'HEADER'
services:
HEADER

COMPOSE_SERVICES=()
PG_PASSWORD=""
MAILHOG_SELECTED=false

for svc in $SERVICES; do
  case "$svc" in
    1)
      if [[ -z "$NPM_BIND" ]]; then
        fail "Nginx Proxy Manager selected, but no Tailscale IPv4 was found on this host."
        echo "  Bring the tailnet up first (the host provisioning does this), or choose the" >&2
        echo "  address deliberately:  SETUP_NPM_BIND=<ip> bash scripts/setup/setup-dockers.sh" >&2
        echo "  Publishing it on 0.0.0.0 is not offered — Docker bypasses UFW, so that would put" >&2
        echo "  80/443/81 on every interface the host has." >&2
        exit 1
      fi
      COMPOSE_SERVICES+=("nginx-proxy-manager")
      cat >> "$COMPOSE_DIR/docker-compose.yaml" <<SVC
  nginx-proxy-manager:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "$NPM_BIND:80:80"
      - "$NPM_BIND:443:443"
      - "$NPM_BIND:81:81"
    volumes:
      - ./npm_data:/data
      - ./npm_letsencrypt:/etc/letsencrypt
    networks:
      - $DOCKER_NETWORK

SVC
      ;;
    2)
      COMPOSE_SERVICES+=("postgres")
      PG_DEFAULT_PASS=$(openssl rand -base64 36 | tr -d '/+=' | head -c 36)
      prompt_value PG_PASSWORD "PostgreSQL password" "$PG_DEFAULT_PASS"
      prompt_value PG_DATABASE "PostgreSQL database name" "officer_dev"
      cat >> "$COMPOSE_DIR/docker-compose.yaml" <<SVC
  postgres:
    image: postgres:18.3-alpine
    container_name: postgres
    restart: unless-stopped
    ports:
      - "127.0.0.1:5432:5432"
    environment:
      POSTGRES_PASSWORD: $PG_PASSWORD
      PGDATA: /var/lib/postgresql/data
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
      - ./db_dumps:/db_dumps
    networks:
      - $DOCKER_NETWORK

SVC
      ;;
    3)
      COMPOSE_SERVICES+=("mailhog")
      MAILHOG_SELECTED=true
      cat >> "$COMPOSE_DIR/docker-compose.yaml" <<SVC
  mailhog:
    image: mailhog/mailhog:latest
    container_name: mailhog
    restart: unless-stopped
    ports:
      - "127.0.0.1:1025:1025"
      - "127.0.0.1:8025:8025"
    networks:
      - $DOCKER_NETWORK

SVC
      ;;
    4)
      COMPOSE_SERVICES+=("redis")
      cat >> "$COMPOSE_DIR/docker-compose.yaml" <<SVC
  redis:
    image: redis:alpine
    container_name: redis
    restart: unless-stopped
    ports:
      - "127.0.0.1:6379:6379"
    volumes:
      - ./redis_data:/data
    networks:
      - $DOCKER_NETWORK

SVC
      ;;
    5)
      COMPOSE_SERVICES+=("searxng")
      mkdir -p "$COMPOSE_DIR/searxng"
      SEARXNG_SECRET=$(openssl rand -hex 32)
      cat > "$COMPOSE_DIR/searxng/settings.yml" <<SEARXCFG
use_default_settings: true

server:
  secret_key: "$SEARXNG_SECRET"
  limiter: false

search:
  formats:
    - html
    - json
SEARXCFG
      cat >> "$COMPOSE_DIR/docker-compose.yaml" <<SVC
  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ./searxng:/etc/searxng
    environment:
      SEARXNG_BASE_URL: http://localhost:8080/
    networks:
      - $DOCKER_NETWORK

SVC
      ;;
  esac
done

# Add networks block
cat >> "$COMPOSE_DIR/docker-compose.yaml" <<FOOTER
networks:
  $DOCKER_NETWORK:
    external: true
FOOTER

# ─── start services ─────────────────────────────────────────────────────────
info "Starting docker compose services..."
(cd "$COMPOSE_DIR" && docker compose up -d) >&2

ok "Docker services: ${COMPOSE_SERVICES[*]}"

# ─── create database if PostgreSQL was selected ─────────────────────────────
if [[ -n "$PG_PASSWORD" ]]; then
  info "Waiting for PostgreSQL to be ready..."
  for i in $(seq 1 15); do
    if docker exec postgres pg_isready -U postgres >/dev/null 2>&1; then
      break
    fi
    sleep 1
  done

  if docker exec postgres pg_isready -U postgres >/dev/null 2>&1; then
    # Create the database if it doesn't exist
    docker exec postgres psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = '$PG_DATABASE'" | grep -q 1 \
      || docker exec postgres psql -U postgres -c "CREATE DATABASE $PG_DATABASE" >/dev/null 2>&1
    ok "Database '$PG_DATABASE' ready"
  else
    warn "PostgreSQL not ready after 15s — create database manually"
  fi
fi

# ─── output parseable values to stdout ───────────────────────────────────────
echo "COMPOSE_DIR=$COMPOSE_DIR"

if [[ " ${COMPOSE_SERVICES[*]} " == *" nginx-proxy-manager "* ]]; then
  echo "NPM_BIND=$NPM_BIND"
fi

if [[ -n "$PG_PASSWORD" ]]; then
  echo "POSTGRES_URL=postgresql://postgres:${PG_PASSWORD}@127.0.0.1:5432/${PG_DATABASE}"
fi

if [[ "$MAILHOG_SELECTED" == true ]]; then
  echo "MAIL_TRANSPORT=smtp://127.0.0.1:1025"
fi
