#!/bin/bash
# ═══════════════════════════════════════════════════════════════════════
#  AliothPress — Cloud Installer
#  Paste this into "User Data" / "Cloud-Init" when creating your server.
#  Works on: Hetzner Cloud, DigitalOcean, Linode, Vultr, AWS, GCP, Azure
#  Requires: Ubuntu 22.04 LTS or newer
# ═══════════════════════════════════════════════════════════════════════
#
#  ╔══════════════════════════════════════════════════════════╗
#  ║  CHANGE THIS — your AliothPress download link:          ║
#  ╚══════════════════════════════════════════════════════════╝
DOWNLOAD_URL="https://updates.aliothpress.com/releases/aliothpress-latest.zip"
#
#  After the server is created (2–3 minutes), open:
#    http://YOUR-SERVER-IP
#  The Setup Wizard will guide you through the rest.
# ═══════════════════════════════════════════════════════════════════════

set -e

APP_USER="aliothpress"
APP_DIR="/home/$APP_USER/app"
SERVICE_NAME="aliothpress"
LOG_DIR="/var/log/$SERVICE_NAME"

log() { echo "[AliothPress] $1"; }

# ── 1. System packages ──────────────────────────────────────────────
log "Installing system packages..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq \
    python3 python3-venv python3-dev \
    build-essential \
    nginx \
    certbot python3-certbot-nginx \
    unzip curl

# ── 2. Create application user ─────────────────────────────────────
log "Creating application user..."
if ! id "$APP_USER" &>/dev/null; then
    useradd -r -m -s /bin/bash "$APP_USER"
fi

# Allow nginx (www-data) to access the Gunicorn socket inside the home dir
usermod -a -G "$APP_USER" www-data
chmod 750 /home/$APP_USER

# ── 3. Download AliothPress ─────────────────────────────────────────
log "Downloading AliothPress..."
mkdir -p "$APP_DIR"
cd /tmp
curl -fsSL -o aliothpress.zip "$DOWNLOAD_URL"
unzip -qo aliothpress.zip -d aliothpress_extract

# Handle zip with or without top-level folder
EXTRACTED=$(ls aliothpress_extract)
if [ "$(echo "$EXTRACTED" | wc -l)" -eq 1 ] && [ -d "aliothpress_extract/$EXTRACTED" ]; then
    cp -r aliothpress_extract/$EXTRACTED/* "$APP_DIR/"
else
    cp -r aliothpress_extract/* "$APP_DIR/"
fi
rm -rf aliothpress.zip aliothpress_extract

# ── 4. Virtual environment & dependencies ───────────────────────────
log "Setting up Python environment..."
cd "$APP_DIR"
python3 -m venv venv
venv/bin/pip install --upgrade pip --quiet
venv/bin/pip install -r requirements.txt --quiet
venv/bin/pip install gunicorn --quiet

# ── 5. Create .env ──────────────────────────────────────────────────
log "Creating configuration..."
if [ ! -f .env ]; then
    SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
    cat > .env << ENVEOF
SECRET_KEY=$SECRET
DATABASE_URI=sqlite:///aliothpress.db
UPDATE_SERVER_URL=https://updates.aliothpress.com
SERVICE_NAME=$SERVICE_NAME.service
ENVEOF
fi

# ── 6. Create version.txt if missing ───────────────────────────────
if [ ! -f version.txt ]; then
    echo "1.0.0" > version.txt
fi

# ── 7. Fix ownership ───────────────────────────────────────────────
chown -R "$APP_USER:$APP_USER" "$APP_DIR"

# ── 8. Gunicorn + systemd ──────────────────────────────────────────
log "Configuring background service..."
WORKERS=$(python3 -c "import os; print(max(2, min(4, (os.cpu_count() or 1) * 2 + 1)))")
SOCKET="$APP_DIR/$SERVICE_NAME.sock"

mkdir -p "$LOG_DIR"
chown "$APP_USER:$APP_USER" "$LOG_DIR"

cat > /etc/systemd/system/$SERVICE_NAME.service << SVCEOF
[Unit]
Description=AliothPress CMS
After=network.target

[Service]
Type=notify
User=$APP_USER
Group=$APP_USER
UMask=0002
WorkingDirectory=$APP_DIR
Environment="PATH=$APP_DIR/venv/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=$APP_DIR/venv/bin/gunicorn \
    --workers $WORKERS \
    --preload \
    --bind unix:$SOCKET \
    --timeout 120 \
    --access-logfile $LOG_DIR/access.log \
    --error-logfile $LOG_DIR/error.log \
    app:app
ExecReload=/bin/kill -s HUP \$MAINPID
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
SVCEOF

# ── 9. Nginx ────────────────────────────────────────────────────────
log "Configuring web server..."

# Auto-detect server IP
SERVER_IP=$(curl -4 -s --max-time 5 https://api.ipify.org || curl -4 -s --max-time 5 https://ifconfig.me || echo "_")

cat > /etc/nginx/sites-available/$SERVICE_NAME << NGXEOF
server {
    listen 80 default_server;
    server_name $SERVER_IP _;

    client_max_body_size 20M;

    location /static/ {
        alias $APP_DIR/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location /favicon.ico {
        alias $APP_DIR/static/uploads/favicon/favicon.ico;
        expires 30d;
    }

    location / {
        proxy_pass http://unix:$SOCKET;
        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 \$scheme;
        proxy_read_timeout 120;
    }
}
NGXEOF

ln -sf /etc/nginx/sites-available/$SERVICE_NAME /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default

# ── 9b. Compression drop-in (gzip for text assets) ─────────────────
# Ubuntu's stock nginx.conf has `gzip on;` but leaves `gzip_types`
# commented out, so by default ONLY text/html is compressed and the
# CSS/JS bundles ship uncompressed. This drop-in lives in conf.d/
# (included at the http{} level), so it applies to EVERY server block:
# the initial IP-only host now, AND the 443/www blocks that certbot
# adds later via aliothpress-ssl. It never edits the site's server
# block, so it cannot interfere with the browser install wizard or the
# "Enable HTTPS" flow. woff2 is intentionally NOT listed, it is already
# compressed internally, so re-compressing it only wastes CPU.
# Note: `gzip on;` is already active in Ubuntu's stock nginx.conf, so we
# do NOT repeat it here. Repeating it in the same http{} context makes
# nginx report "gzip directive is duplicate" and refuse to start.
cat > /etc/nginx/conf.d/aliothpress-gzip.conf << 'GZEOF'
# AliothPress — gzip for text assets (CSS/JS/SVG/JSON/XML/HTML).
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/css application/javascript application/json image/svg+xml application/rss+xml text/xml application/xml;
GZEOF

# ── 10. SSL helper script (used by admin panel "Enable HTTPS") ──────
log "Installing SSL helper..."
cat > /usr/local/bin/aliothpress-ssl << 'SSLEOF'
#!/bin/bash
# ─────────────────────────────────────────────────────────────────────
# AliothPress SSL Helper — called by the admin panel, runs as root.
# Usage: aliothpress-ssl <domain> <email>
# Only the aliothpress user can call this via sudoers.
# ─────────────────────────────────────────────────────────────────────
set -e

DOMAIN="$1"
EMAIL="$2"
SERVICE_NAME="aliothpress"
NGINX_CONF="/etc/nginx/sites-available/$SERVICE_NAME"

# ── Validate inputs (defense in depth — Flask also validates) ───────
if [ -z "$DOMAIN" ] || [ -z "$EMAIL" ]; then
    echo "ERROR: Usage: aliothpress-ssl <domain> <email>"
    exit 1
fi

if ! echo "$DOMAIN" | grep -qP '^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?)*\.[a-z]{2,}$'; then
    echo "ERROR: Invalid domain format"
    exit 1
fi

if [ ${#DOMAIN} -gt 253 ]; then
    echo "ERROR: Domain too long"
    exit 1
fi

if [ ! -f "$NGINX_CONF" ]; then
    echo "ERROR: Nginx config not found at $NGINX_CONF"
    exit 1
fi

# ── Verify DNS points to this server for BOTH apex and www ──────────
# Using getent (part of glibc, always available) instead of dig.
# Both apex (example.com) and www (www.example.com) must resolve to
# this server's public IP before certbot can issue a certificate.
SERVER_IP=$(curl -4 -s --max-time 5 https://api.ipify.org || curl -4 -s --max-time 5 https://ifconfig.me || echo "")
if [ -z "$SERVER_IP" ]; then
    echo "ERROR: DNS_SERVER_IP_UNKNOWN"
    exit 1
fi

APEX_IP=$(getent hosts "$DOMAIN" 2>/dev/null | awk '{print $1}' | head -1)
if [ -z "$APEX_IP" ] || [ "$APEX_IP" != "$SERVER_IP" ]; then
    echo "ERROR: DNS_APEX_MISMATCH"
    exit 1
fi

WWW_IP=$(getent hosts "www.$DOMAIN" 2>/dev/null | awk '{print $1}' | head -1)
if [ -z "$WWW_IP" ] || [ "$WWW_IP" != "$SERVER_IP" ]; then
    echo "ERROR: DNS_WWW_MISSING"
    exit 1
fi

# ── Backup nginx config ────────────────────────────────────────────
cp "$NGINX_CONF" "$NGINX_CONF.bak"

# ── Update server_name to include both apex and www ────────────────
# certbot reads server_name to know which domains to request a cert for.
sed -i "s/server_name[[:space:]]\+[^;]*/server_name $DOMAIN www.$DOMAIN/" "$NGINX_CONF"

# ── Test nginx config ──────────────────────────────────────────────
if ! nginx -t 2>/dev/null; then
    cp "$NGINX_CONF.bak" "$NGINX_CONF"
    echo "ERROR: Nginx config test failed — rolled back"
    exit 1
fi

# ── Reload nginx (domain must be active before certbot runs) ───────
systemctl reload nginx

# ── Run certbot for both apex and www ──────────────────────────────
# --redirect: certbot adds its own 80→443 redirect.
# www → apex canonicalization is handled by a small server block we add below.
CERTBOT_OUTPUT=$(certbot --nginx -d "$DOMAIN" -d "www.$DOMAIN" \
    --non-interactive --agree-tos --email "$EMAIL" \
    --expand --redirect 2>&1) || {
    # certbot failed — restore nginx config and report
    cp "$NGINX_CONF.bak" "$NGINX_CONF"
    systemctl reload nginx 2>/dev/null || true
    echo "ERROR: Certbot failed: $CERTBOT_OUTPUT"
    exit 1
}

# ── Add www → apex 301 redirect in a separate server block ─────────
# certbot issued a single certificate covering both names, but by default
# it configured both to serve the same content. We want www.$DOMAIN to
# redirect to $DOMAIN (canonical form) instead.
# Insert a dedicated server block for www that 301-redirects to apex.
REDIRECT_MARKER="# aliothpress-www-redirect"
if ! grep -q "$REDIRECT_MARKER" "$NGINX_CONF"; then
    cat >> "$NGINX_CONF" << WWWREDIR

$REDIRECT_MARKER
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.$DOMAIN;

    ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;

    return 301 https://$DOMAIN\$request_uri;
}
WWWREDIR

    # Remove www.$DOMAIN from the main (apex) server_name so that
    # the new www server block above wins for www.$DOMAIN traffic.
    sed -i "0,/server_name $DOMAIN www.$DOMAIN/s//server_name $DOMAIN/" "$NGINX_CONF"

    # Flatten Certbot's HTTP→HTTPS redirect for www: by default Certbot emits
    # \`if (\$host = www.$DOMAIN) { return 301 https://\$host\$request_uri; }\`,
    # which sends http://www → https://www, then our www-redirect block above
    # adds a second hop to https://apex. Two hops is bad for SEO. Replace
    # \$host with the apex domain inside the www-branch only so that
    # http://www → https://apex in a single 301. The apex branch (if (\$host = $DOMAIN))
    # is left untouched.
    python3 - "$NGINX_CONF" "$DOMAIN" <<'PYEOF'
import re, sys
path, domain = sys.argv[1], sys.argv[2]
with open(path, 'r') as f:
    conf = f.read()
pattern = re.compile(
    r'(if\s*\(\s*\$host\s*=\s*www\.' + re.escape(domain) + r'\s*\)\s*\{\s*return\s+301\s+https://)\$host(\$request_uri\s*;\s*\})'
)
new_conf, n = pattern.subn(r'\g<1>' + domain + r'\g<2>', conf)
if n:
    with open(path, 'w') as f:
        f.write(new_conf)
PYEOF

    if ! nginx -t 2>/dev/null; then
        cp "$NGINX_CONF.bak" "$NGINX_CONF"
        systemctl reload nginx 2>/dev/null || true
        echo "ERROR: Nginx config test failed after www redirect — rolled back"
        exit 1
    fi
    systemctl reload nginx
fi

echo "OK: HTTPS enabled for $DOMAIN and www.$DOMAIN"
exit 0
SSLEOF
chmod 755 /usr/local/bin/aliothpress-ssl

# ── 10b. Update-prep helper (used by updater before applying updates) ──
# Restores ownership of app files to the service user. Needed because the
# updater runs as $APP_USER (via gunicorn) but can't chown files that may
# have been edited by root via SSH/SFTP. Paths are hardcoded — the helper
# accepts no arguments, so it can't be used to chown arbitrary directories.
log "Installing update-prep helper..."
cat > /usr/local/bin/aliothpress-update-prep << 'UPDEOF'
#!/bin/bash
# ─────────────────────────────────────────────────────────────────────
# AliothPress Update-Prep Helper — called by the updater, runs as root.
# Usage: aliothpress-update-prep  (no arguments)
# Only the aliothpress user can call this via sudoers.
# ─────────────────────────────────────────────────────────────────────
set -e

APP_DIR="/home/aliothpress/app"
APP_USER="aliothpress"

# Hardcoded paths — no argument injection possible.
if [ ! -d "$APP_DIR" ]; then
    echo "ERROR: App directory not found at $APP_DIR"
    exit 1
fi

if ! id -u "$APP_USER" >/dev/null 2>&1; then
    echo "ERROR: Service user $APP_USER not found"
    exit 1
fi

chown -R "$APP_USER:$APP_USER" "$APP_DIR"
echo "OK: Ownership restored on $APP_DIR"
exit 0
UPDEOF
chmod 755 /usr/local/bin/aliothpress-update-prep

# ── 11. Sudoers — allow aliothpress user to run specific commands ───
cat > /etc/sudoers.d/$SERVICE_NAME << SUDEOF
$APP_USER ALL=(ALL) NOPASSWD: /bin/systemctl restart $SERVICE_NAME.service
$APP_USER ALL=(ALL) NOPASSWD: /bin/systemctl reload $SERVICE_NAME.service
$APP_USER ALL=(postgres) NOPASSWD: /usr/bin/psql
$APP_USER ALL=(ALL) NOPASSWD: /usr/local/bin/aliothpress-ssl
$APP_USER ALL=(ALL) NOPASSWD: /usr/local/bin/aliothpress-update-prep
$APP_USER ALL=(ALL) NOPASSWD: /usr/bin/apt-get update *
$APP_USER ALL=(ALL) NOPASSWD: /usr/bin/apt-get install *
$APP_USER ALL=(ALL) NOPASSWD: /bin/systemctl start postgresql
$APP_USER ALL=(ALL) NOPASSWD: /bin/systemctl enable postgresql
SUDEOF
chmod 440 /etc/sudoers.d/$SERVICE_NAME

# ── 12. Start everything ───────────────────────────────────────────
log "Starting services..."
systemctl daemon-reload
systemctl enable $SERVICE_NAME
systemctl start $SERVICE_NAME
systemctl restart nginx

log "═══════════════════════════════════════════════════════"
log "  AliothPress is ready!"
log "  Open in your browser: http://$SERVER_IP"
log "═══════════════════════════════════════════════════════"
