#!/usr/bin/env bash
# naiveproxy-bootstrap v1.0
# One-shot provisioning of a NaiveProxy (Caddy + forwardproxy) server.
# Supports: Debian 12+, Ubuntu 22.04+. Run as root.

set -euo pipefail

VERSION="1.0"

log()  { echo; echo "[$(date +%H:%M:%S)] $*"; }
ok()   { echo "  [OK]   $*"; }
warn() { echo "  [WARN] $*"; }
die()  { echo; echo "ERROR: $*" >&2; exit 1; }

STEP="init"
trap 'die "Failed at line $LINENO during step: ${STEP:-unknown}"' ERR

# === Preflight ===
STEP="preflight"

[[ $EUID -ne 0 ]] && die "Must run as root on a fresh VPS"
command -v apt-get >/dev/null 2>&1 || die "Supports only Debian/Ubuntu family"

export DEBIAN_FRONTEND=noninteractive

log "naiveproxy-bootstrap v$VERSION"

if ! command -v sudo >/dev/null 2>&1; then
    ok "installing sudo (missing on this image)"
    apt-get update -qq
    apt-get install -y -qq sudo
fi

apt-get install -y -qq lsb-release >/dev/null 2>&1 || true
OS_ID=$(lsb_release -si 2>/dev/null | tr 'A-Z' 'a-z' || echo unknown)
OS_CODENAME=$(lsb_release -sc 2>/dev/null || echo unknown)
case "$OS_ID" in
    debian|ubuntu) ok "OS: $OS_ID $OS_CODENAME" ;;
    *) die "Unsupported OS: $OS_ID (need Debian 12+ or Ubuntu 22.04+)" ;;
esac

PUBLIC_IP=""
for svc in ifconfig.me api.ipify.org ipinfo.io/ip icanhazip.com; do
    PUBLIC_IP=$(curl -fsS -4 --max-time 5 "https://$svc" 2>/dev/null || true)
    [ -n "$PUBLIC_IP" ] && break
done
[ -n "$PUBLIC_IP" ] || die "Cannot auto-detect public IP"
ok "Public IP: $PUBLIC_IP"

# === Banner ===
cat <<'BANNER'

===================================================================
  NaiveProxy Server Bootstrap
===================================================================
  This will set up a NaiveProxy server on this VPS:

    1. Create 'default' user with SSH key (root SSH disabled)
    2. Harden OS: updates, SSH, UFW, fail2ban, journald
    3. Build Caddy with forwardproxy (naive) plugin
    4. Deploy vpn-agent for user management API
    5. Open firewall: 22/tcp, 443/tcp, 80/tcp (ACME)
    6. Start services and verify

  After completion, point your domain's DNS A record to this
  server's IP (no CF proxy / grey cloud) and configure your
  Slack bot to manage users via the /api endpoint.
===================================================================

BANNER

read_tty() {
    local prompt="$1"
    local val
    read -r -p "$prompt" val < /dev/tty
    echo "$val"
}
confirm_tty() {
    local reply
    read -r -n 1 -p "Continue? [y/N]: " reply < /dev/tty
    echo
    [[ "$reply" =~ ^[Yy]$ ]]
}

confirm_tty || die "Aborted by user"

# === Input collection ===
STEP="input collection"

SSH_PUBKEY="${NODE_SSH_PUBKEY:-}"
DOMAIN="${NODE_DOMAIN:-}"
AGENT_TOKEN="${NODE_AGENT_TOKEN:-}"

if [ -z "$SSH_PUBKEY" ]; then
    echo
    echo "Step 1/3: SSH public key for user 'default'"
    echo "  One line, starts with 'ssh-rsa' or 'ssh-ed25519'."
    SSH_PUBKEY=$(read_tty "  > ")
fi
case "$SSH_PUBKEY" in
    "ssh-rsa "*|"ssh-ed25519 "*|"ssh-dss "*|"ecdsa-"*) ok "SSH pubkey looks valid" ;;
    *) die "SSH pubkey format is wrong" ;;
esac

if [ -z "$DOMAIN" ]; then
    echo
    echo "Step 2/3: Domain name for this server"
    echo "  DNS A record must point to $PUBLIC_IP (no CF proxy)."
    echo "  Example: vpn.example.com"
    DOMAIN=$(read_tty "  > ")
fi
[ -n "$DOMAIN" ] || die "Domain is required"
ok "Domain: $DOMAIN"

if [ -z "$AGENT_TOKEN" ]; then
    echo
    echo "Step 3/3: Agent API token"
    echo "  This token authenticates your Slack bot to manage users."
    echo "  Generate one: openssl rand -base64 32"
    echo "  Or press Enter to auto-generate."
    AGENT_TOKEN=$(read_tty "  > ")
    if [ -z "$AGENT_TOKEN" ]; then
        AGENT_TOKEN=$(openssl rand -base64 32)
        ok "Auto-generated token: $AGENT_TOKEN"
        echo "  Save this token — you'll need it for the Slack bot config."
    fi
fi
ok "Agent token set (${#AGENT_TOKEN} chars)"

echo
log "Inputs collected, starting provisioning"

# === Step A: default user ===
STEP="user creation"
log "Creating 'default' user"

if ! id default >/dev/null 2>&1; then
    useradd -m -s /bin/bash default
    ok "user created"
else
    ok "user already exists"
fi
passwd -l default >/dev/null 2>&1 || true

install -d -o default -g default -m 700 /home/default/.ssh
touch /home/default/.ssh/authorized_keys
chown default:default /home/default/.ssh/authorized_keys
chmod 600 /home/default/.ssh/authorized_keys

if ! grep -qxF "$SSH_PUBKEY" /home/default/.ssh/authorized_keys; then
    echo "$SSH_PUBKEY" >> /home/default/.ssh/authorized_keys
    ok "SSH key appended"
else
    ok "SSH key already present"
fi

cat > /etc/sudoers.d/90-default-nopasswd <<'EOF'
default ALL=(ALL) NOPASSWD:ALL
EOF
chmod 0440 /etc/sudoers.d/90-default-nopasswd
visudo -cf /etc/sudoers.d/90-default-nopasswd >/dev/null || die "sudoers validation failed"
ok "sudoers configured"

for u in debian ubuntu; do
    if id "$u" >/dev/null 2>&1 && [ "$u" != "default" ]; then
        pkill -KILL -u "$u" 2>/dev/null || true
        sleep 1
        userdel -r "$u" >/dev/null 2>&1 || true
        ok "removed cloud-init user: $u"
    fi
done
rm -f /etc/sudoers.d/90-cloud-init-users

# === Step B: system update ===
STEP="system update"
log "Updating packages"
apt-get update -qq
apt-get upgrade -y -qq >/dev/null
apt-get autoremove -y -qq >/dev/null
ok "system updated"

# === Step C: base packages ===
STEP="base packages"
log "Installing base packages"
apt-get install -y -qq \
    curl wget git vim nano htop jq ncdu \
    net-tools dnsutils ca-certificates \
    gnupg apt-transport-https software-properties-common \
    unzip ufw fail2ban unattended-upgrades tzdata \
    needrestart >/dev/null
ok "base packages installed"

# === Step D: timezone ===
STEP="timezone"
timedatectl set-timezone UTC
ok "timezone: UTC"

# === Step E: kernel tuning ===
STEP="sysctl tuning"
log "Applying sysctl tuning"

cat > /etc/sysctl.d/99-naive-node.conf <<'EOF'
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
fs.file-max = 1048576
net.ipv4.tcp_fastopen = 3
EOF

modprobe tcp_bbr 2>/dev/null || warn "tcp_bbr module load failed"
echo tcp_bbr > /etc/modules-load.d/bbr.conf
sysctl -p /etc/sysctl.d/99-naive-node.conf >/dev/null
ok "sysctl tuning applied (BBR + fq)"

# === Step F: unattended upgrades ===
STEP="unattended upgrades"
log "Enabling unattended security upgrades"

cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
EOF

cat > /etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
Unattended-Upgrade::Origins-Pattern {
    "origin=Debian,codename=${distro_codename},label=Debian";
    "origin=Debian,codename=${distro_codename},label=Debian-Security";
    "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
    "origin=Ubuntu,archive=${distro_codename}-security";
};
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
EOF

systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true
ok "unattended upgrades enabled"

# === Step G: SSH hardening ===
STEP="SSH hardening"
log "Hardening sshd"

rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf

cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
EOF

if sshd -t; then
    systemctl restart ssh
    ok "sshd hardened, root SSH disabled"
else
    rm -f /etc/ssh/sshd_config.d/99-hardening.conf
    die "sshd config validation failed, rolled back"
fi

# === Step H: UFW ===
STEP="UFW"
log "Configuring UFW"

ufw default deny incoming >/dev/null
ufw default allow outgoing >/dev/null
ufw limit 22/tcp comment 'SSH (rate-limited)' >/dev/null
ufw allow 443/tcp comment 'NaiveProxy + ACME' >/dev/null
ufw allow 80/tcp comment 'ACME HTTP challenge' >/dev/null
ufw --force enable >/dev/null 2>&1
ok "UFW active: 22/tcp rate-limited, 443/tcp, 80/tcp"

# === Step I: fail2ban ===
STEP="fail2ban"
log "Configuring fail2ban"

cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 3
findtime = 10m
bantime = 1h
EOF

systemctl enable --now fail2ban >/dev/null 2>&1 || true
systemctl restart fail2ban
sleep 2
if systemctl is-active fail2ban >/dev/null; then
    ok "fail2ban running"
else
    warn "fail2ban not active"
fi

# === Step J: journald cap ===
STEP="journald"
mkdir -p /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/size.conf <<'EOF'
[Journal]
SystemMaxUse=500M
SystemKeepFree=1G
EOF
systemctl restart systemd-journald
ok "journald capped at 500M"

# === Step K: Install Go ===
STEP="golang"
log "Installing Go (for building Caddy)"

GO_VERSION="1.23.8"
curl -Lo /tmp/go.tar.gz "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz
rm -f /tmp/go.tar.gz
export PATH=$PATH:/usr/local/go/bin
ok "Go $(/usr/local/go/bin/go version | awk '{print $3}')"

# === Step L: Build Caddy with forwardproxy ===
STEP="caddy build"
log "Building Caddy with NaiveProxy plugin (1-2 min)"

export PATH=$PATH:/usr/local/go/bin:/root/go/bin
GOBIN=/usr/local/bin go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest 2>/dev/null
xcaddy build --with github.com/caddyserver/forwardproxy=github.com/klzgrad/forwardproxy@naive 2>/dev/null
mv caddy /usr/local/bin/caddy
chmod +x /usr/local/bin/caddy
ok "Caddy $(/usr/local/bin/caddy version | head -1)"

# === Step M: Install Node.js ===
STEP="nodejs"
log "Installing Node.js (for vpn-agent)"

if ! command -v node >/dev/null 2>&1 || [ "$(node -v | sed 's/v//' | cut -d. -f1)" -lt 18 ]; then
    curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
    apt-get install -y -qq nodejs >/dev/null
fi
ok "Node.js $(node -v)"

# === Step N: Caddy config ===
STEP="caddy config"
log "Writing Caddy configuration"

ADMIN_PASS=$(openssl rand -base64 24)

mkdir -p /etc/caddy /var/www/html /opt/vpn-agent

# Empty vpn-users file
touch /etc/caddy/vpn-users

# Caddyfile
cat > /etc/caddy/Caddyfile <<CADDYEOF
{
  order forward_proxy before file_server
  admin off
}

:443, $DOMAIN {
  tls internal@$DOMAIN

  forward_proxy {
    basic_auth _admin_ $ADMIN_PASS
    import /etc/caddy/vpn-users
    hide_ip
    hide_via
    probe_resistance
  }

  @api {
    path /api/*
    header Authorization "Bearer $AGENT_TOKEN"
  }
  handle @api {
    reverse_proxy 127.0.0.1:3000
  }

  @api_unauth {
    path /api/*
    not header Authorization "Bearer $AGENT_TOKEN"
  }
  handle @api_unauth {
    respond 404
  }

  file_server {
    root /var/www/html
  }
}
CADDYEOF

chmod 600 /etc/caddy/Caddyfile
ok "Caddyfile written"

# Site placeholder
cat > /var/www/html/index.html <<'SITEEOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Moraine Systems</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0e17; color: #c9d1d9; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
    .c { max-width: 440px; text-align: center; padding: 2rem; }
    h1 { color: #58a6ff; font-weight: 300; font-size: 2rem; margin-bottom: .5rem; }
    p { color: #8b949e; font-size: .95rem; }
    footer { margin-top: 2rem; font-size: .75rem; color: #484f58; }
  </style>
</head>
<body>
  <div class="c">
    <h1>Moraine Systems</h1>
    <p>Infrastructure services</p>
    <footer>&copy; 2026 Moraine Systems Ltd.</footer>
  </div>
</body>
</html>
SITEEOF
ok "site placeholder written"

# === Step O: vpn-agent ===
STEP="vpn-agent"
log "Deploying vpn-agent"

cat > /opt/vpn-agent/package.json <<'AGENTEOF'
{
  "name": "vpn-agent",
  "version": "1.0.0",
  "description": "Lightweight agent for managing NaiveProxy users on Caddy server",
  "main": "app.mjs",
  "type": "module",
  "scripts": {
    "start": "node app.mjs"
  }
}
AGENTEOF

cat > /opt/vpn-agent/app.mjs <<'AGENTEOF'
import {createServer} from 'node:http';
import {syncUsers, getUsers} from './caddy.mjs';

const PORT = process.env.PORT || 3000;
const TOKEN = process.env.AGENT_TOKEN;

if (!TOKEN) {
  console.error('AGENT_TOKEN env var is required');
  process.exit(1);
}

function authorize(req) {
  return req.headers['authorization'] === `Bearer ${TOKEN}`;
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    let data = '';
    req.on('data', chunk => data += chunk);
    req.on('end', () => {
      try {
        resolve(data ? JSON.parse(data) : {});
      } catch {
        reject(new Error('Invalid JSON'));
      }
    });
    req.on('error', reject);
  });
}

function json(res, status, body) {
  res.writeHead(status, {'Content-Type': 'application/json'});
  res.end(JSON.stringify(body));
}

const server = createServer(async (req, res) => {
  if (!authorize(req)) return json(res, 401, {error: 'unauthorized'});

  const path = new URL(req.url, `http://localhost:${PORT}`).pathname;

  try {
    if (req.method === 'POST' && path === '/api/sync') {
      const body = await readBody(req);
      if (!Array.isArray(body.users)) return json(res, 400, {error: 'users array is required'});

      for (const u of body.users) {
        if (!u.username || !u.password) return json(res, 400, {error: 'each user must have username and password'});
      }

      const result = syncUsers(body.users);
      if (result.error) return json(res, 500, result);
      return json(res, 200, result);
    }

    if (req.method === 'GET' && path === '/api/status') {
      const users = getUsers();
      return json(res, 200, {ok: true, users: users.length});
    }

    json(res, 404, {error: 'not found'});
  } catch (e) {
    console.error(e);
    json(res, 500, {error: 'internal error'});
  }
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`VPN Agent listening on 127.0.0.1:${PORT}`);
});
AGENTEOF

cat > /opt/vpn-agent/caddy.mjs <<'AGENTEOF'
import {readFileSync, writeFileSync, existsSync} from 'node:fs';
import {execSync} from 'node:child_process';

const USERS_FILE = process.env.USERS_FILE || '/etc/caddy/vpn-users';
const CADDYFILE = process.env.CADDYFILE || '/etc/caddy/Caddyfile';

export function syncUsers(users) {
  const backup = existsSync(USERS_FILE) ? readFileSync(USERS_FILE, 'utf-8') : '';

  const lines = users.map(({username, password}) => `basic_auth ${username} ${password}`);
  const content = lines.join('\n') + (lines.length ? '\n' : '');

  if (content === backup) return {success: true, count: users.length, changed: false};

  writeFileSync(USERS_FILE, content);

  const result = validateAndReload(backup);
  if (result.error) return result;

  return {success: true, count: users.length, changed: true};
}

export function getUsers() {
  if (!existsSync(USERS_FILE)) return [];
  const content = readFileSync(USERS_FILE, 'utf-8');
  return content
    .split('\n')
    .map(line => line.trim())
    .filter(line => line.startsWith('basic_auth '))
    .map(line => {
      const parts = line.split(/\s+/);
      return {username: parts[1], password: parts[2]};
    });
}

function validateAndReload(rollbackContent) {
  try {
    execSync(`/usr/local/bin/caddy validate --config ${CADDYFILE}`, {stdio: 'pipe'});
  } catch (e) {
    writeFileSync(USERS_FILE, rollbackContent);
    return {error: 'validate-failed', details: e.stderr?.toString()};
  }

  try {
    execSync('systemctl restart caddy', {stdio: 'pipe'});
  } catch (e) {
    return {error: 'restart-failed', details: e.stderr?.toString()};
  }

  return {success: true};
}
AGENTEOF

ok "vpn-agent deployed to /opt/vpn-agent/"

# === Step P: systemd services ===
STEP="systemd"
log "Creating systemd services"

cat > /etc/systemd/system/caddy.service <<'EOF'
[Unit]
Description=Caddy NaiveProxy
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/vpn-agent.service <<AGENTSVC
[Unit]
Description=VPN Agent
After=caddy.service

[Service]
ExecStart=/usr/bin/node /opt/vpn-agent/app.mjs
Restart=on-failure
RestartSec=5
WorkingDirectory=/opt/vpn-agent
Environment=AGENT_TOKEN=$AGENT_TOKEN
Environment=PORT=3000

[Install]
WantedBy=multi-user.target
AGENTSVC

systemctl daemon-reload
ok "systemd services created"

# === Step Q: Start services ===
STEP="start services"
log "Starting services"

systemctl enable --now caddy
sleep 3
if systemctl is-active caddy >/dev/null; then
    ok "Caddy running"
else
    warn "Caddy not active — checking logs"
    journalctl -u caddy --no-pager -n 10
    die "Caddy failed to start"
fi

systemctl enable --now vpn-agent
sleep 2
if systemctl is-active vpn-agent >/dev/null; then
    ok "vpn-agent running"
else
    warn "vpn-agent not active"
fi

# === Step R: verification ===
STEP="verification"
log "Verifying"

if ss -tln 2>/dev/null | grep -q ':443 '; then
    ok "port 443 listening — Caddy is serving"
else
    warn "port 443 not listening"
fi

if ss -tln 2>/dev/null | grep -q ':3000 '; then
    ok "port 3000 listening — vpn-agent is up"
else
    warn "port 3000 not listening"
fi

# NTP
systemctl enable --now systemd-timesyncd >/dev/null 2>&1 || true
for _ in 1 2 3 4 5; do
    [ "$(timedatectl show -p NTPSynchronized --value 2>/dev/null)" = "yes" ] && break
    sleep 2
done
if [ "$(timedatectl show -p NTPSynchronized --value 2>/dev/null)" = "yes" ]; then
    ok "NTP synchronized"
else
    warn "NTP not synchronized"
fi

# === Summary ===
STEP="done"
cat <<SUMMARY

===================================================================
  NaiveProxy Bootstrap complete
===================================================================
  Public IP     : $PUBLIC_IP
  Domain        : $DOMAIN
  Caddy         : $(/usr/local/bin/caddy version 2>/dev/null | head -1)
  Node.js       : $(node -v 2>/dev/null)
  Agent token   : $AGENT_TOKEN

  Ports:
    443/tcp  — NaiveProxy (HTTPS proxy) + site + API
    80/tcp   — ACME HTTP challenge (auto-renew)
    22/tcp   — SSH (key-only, root disabled)

  Files:
    /etc/caddy/Caddyfile     — main config
    /etc/caddy/vpn-users     — user credentials (managed by agent)
    /opt/vpn-agent/          — management API
    /var/www/html/           — site placeholder

  Next steps:
    1. Ensure DNS A record: $DOMAIN → $PUBLIC_IP (no CF proxy)
    2. Configure Slack bot with:
         API URL:   https://$DOMAIN/api
         Token:     $AGENT_TOKEN
    3. Bot adds users via POST /api/sync
    4. Distribute RB Proxy extension to team
         Proxy:     https://$DOMAIN:443

  Test manually:
    curl https://$DOMAIN/              # should show site
    curl -H "Authorization: Bearer $AGENT_TOKEN" https://$DOMAIN/api/status

  Useful commands:
    sudo systemctl status caddy
    sudo systemctl status vpn-agent
    sudo journalctl -u caddy -n 50
    cat /etc/caddy/vpn-users
===================================================================
SUMMARY

if [ -f /var/run/reboot-required ]; then
    echo
    echo "!! Kernel was upgraded — reboot recommended: sudo reboot"
fi
