#!/usr/bin/env bash
set -euo pipefail

# Block Cross Private Access - one file VPS setup.
# Edit these values before running, or pass them as environment variables:
#   sudo ./full-vps-setup.sh

SERVER_PUBLIC_IP="${SERVER_PUBLIC_IP:-68.183.176.97}"
WG_INTERFACE="${WG_INTERFACE:-wg0}"
WG_PORT="${WG_PORT:-51820}"
WG_SUBNET="${WG_SUBNET:-10.8.0.0/24}"
WG_SERVER_IP="${WG_SERVER_IP:-10.8.0.1/24}"
WG_SUBNET_PREFIX="${WG_SUBNET_PREFIX:-10.8.0}"
CLIENT_START="${CLIENT_START:-2}"
CLIENT_END="${CLIENT_END:-254}"
DNS_SERVER="${DNS_SERVER:-1.1.1.1}"

PRIVATE_APP_BIND_IP="${PRIVATE_APP_BIND_IP:-10.8.0.1}"
PRIVATE_APP_PORT="${PRIVATE_APP_PORT:-9090}"
PRIVATE_UPSTREAM_URL="${PRIVATE_UPSTREAM_URL:-http://127.0.0.1:9000}"
PRIVATE_UPSTREAM_PUBLIC_PORT="${PRIVATE_UPSTREAM_PUBLIC_PORT:-9000}"
PRIVATE_APP_SERVICE="${PRIVATE_APP_SERVICE:-blockcross-private-proxy}"

PROVISION_PORT="${PROVISION_PORT:-8088}"
PROVISION_TOKEN="${PROVISION_TOKEN:-c419dcf686e8435db0cb1a220144e1d0}"
PROVISION_INSTALL_DIR="${PROVISION_INSTALL_DIR:-/opt/blockcross-provisioning}"

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

validate_config() {
  if [[ -z "${SERVER_PUBLIC_IP}" ]]; then
    echo "SERVER_PUBLIC_IP is required"
    exit 1
  fi

  if [[ -z "${PROVISION_TOKEN}" ]]; then
    echo "PROVISION_TOKEN is required"
    exit 1
  fi
}

install_packages() {
  apt-get update
  DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard ufw iptables nginx python3 curl
}

setup_wireguard() {
  install -d -m 700 /etc/wireguard

  if [[ ! -f /etc/wireguard/server.key ]]; then
    umask 077
    wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
  fi

  local server_private_key
  server_private_key="$(tr -d '[:space:]' < /etc/wireguard/server.key)"
  echo "${server_private_key}" | wg pubkey > /etc/wireguard/server.pub

  local peer_backup=""
  if [[ -f "/etc/wireguard/${WG_INTERFACE}.conf" ]]; then
    peer_backup="$(awk 'BEGIN{copy=0} /^\[Peer\]/{copy=1} copy{print}' "/etc/wireguard/${WG_INTERFACE}.conf")"
    cp "/etc/wireguard/${WG_INTERFACE}.conf" "/etc/wireguard/${WG_INTERFACE}.conf.bak.$(date +%Y%m%d%H%M%S)"
  fi

  cat > "/etc/wireguard/${WG_INTERFACE}.conf" <<EOF
[Interface]
Address = ${WG_SERVER_IP}
ListenPort = ${WG_PORT}
PrivateKey = ${server_private_key}
SaveConfig = false

PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i ${WG_INTERFACE} -j ACCEPT
PostUp = iptables -A FORWARD -o ${WG_INTERFACE} -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s ${WG_SUBNET} -j MASQUERADE
PostDown = iptables -D FORWARD -i ${WG_INTERFACE} -j ACCEPT
PostDown = iptables -D FORWARD -o ${WG_INTERFACE} -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s ${WG_SUBNET} -j MASQUERADE
EOF

  if [[ -n "${peer_backup}" ]]; then
    printf '\n%s\n' "${peer_backup}" >> "/etc/wireguard/${WG_INTERFACE}.conf"
  fi

  chmod 600 "/etc/wireguard/${WG_INTERFACE}.conf" /etc/wireguard/server.key

  cat > /etc/sysctl.d/99-wireguard-forwarding.conf <<EOF
net.ipv4.ip_forward = 1
EOF

  sysctl --system
  systemctl enable "wg-quick@${WG_INTERFACE}"
  systemctl restart "wg-quick@${WG_INTERFACE}"
}

setup_private_app_proxy() {
  cat > "/etc/nginx/sites-available/${PRIVATE_APP_SERVICE}.conf" <<EOF
server {
    listen ${PRIVATE_APP_BIND_IP}:${PRIVATE_APP_PORT};
    server_name _;

    location / {
        proxy_pass ${PRIVATE_UPSTREAM_URL};
        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 \$scheme;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}
EOF

  ln -sf "/etc/nginx/sites-available/${PRIVATE_APP_SERVICE}.conf" "/etc/nginx/sites-enabled/${PRIVATE_APP_SERVICE}.conf"
  nginx -t
  systemctl enable nginx
  systemctl restart nginx
}

setup_provisioning_server() {
  install -d -m 755 "${PROVISION_INSTALL_DIR}"

  cat > "${PROVISION_INSTALL_DIR}/provisioning-server.py" <<'PY'
#!/usr/bin/env python3
import base64
import ipaddress
import json
import os
import re
import subprocess
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

HOST = os.environ.get("PROVISION_HOST", "0.0.0.0")
PORT = int(os.environ.get("PROVISION_PORT", "8088"))
TOKEN = os.environ.get("PROVISION_TOKEN", "c419dcf686e8435db0cb1a220144e1d0")
WG_INTERFACE = os.environ.get("WG_INTERFACE", "wg0")
WG_CONFIG = Path(os.environ.get("WG_CONFIG", f"/etc/wireguard/{WG_INTERFACE}.conf"))
WG_SUBNET_PREFIX = os.environ.get("WG_SUBNET_PREFIX", "10.8.0")
CLIENT_START = int(os.environ.get("CLIENT_START", "2"))
CLIENT_END = int(os.environ.get("CLIENT_END", "254"))
SERVER_PUBLIC_IP = os.environ.get("SERVER_PUBLIC_IP", "68.183.176.97")
WG_PORT = int(os.environ.get("WG_PORT", "51820"))
DNS_SERVER = os.environ.get("DNS_SERVER", "1.1.1.1")

PUBLIC_KEY_RE = re.compile(r"^[A-Za-z0-9+/]+={0,2}$")

def run(command):
    return subprocess.check_output(command, text=True).strip()

def server_public_key():
    try:
        return run(["wg", "show", WG_INTERFACE, "public-key"])
    except subprocess.CalledProcessError:
        return Path("/etc/wireguard/server.pub").read_text().strip()

def validate_public_key(value):
    if not PUBLIC_KEY_RE.match(value):
        raise ValueError(f"Invalid WireGuard public key format: length={len(value)} value={value!r}")
    try:
        padded_value = value + ("=" * ((4 - len(value) % 4) % 4))
        decoded = base64.b64decode(padded_value, validate=True)
    except Exception as exc:
        raise ValueError(f"Invalid WireGuard public key encoding: length={len(value)} value={value!r}") from exc
    if len(decoded) != 32:
        raise ValueError(f"Invalid WireGuard public key length: {len(decoded)} bytes; value={value!r}")

def current_peers():
    peers = {}
    try:
        output = run(["wg", "show", WG_INTERFACE, "allowed-ips"])
    except subprocess.CalledProcessError:
        output = ""
    for line in output.splitlines():
        parts = line.split()
        if len(parts) >= 2:
            peers[parts[0]] = parts[1]
    if WG_CONFIG.exists():
        public_key = None
        for raw_line in WG_CONFIG.read_text().splitlines():
            line = raw_line.strip()
            if line.startswith("PublicKey"):
                public_key = line.split("=", 1)[1].strip()
            elif line.startswith("AllowedIPs") and public_key:
                peers.setdefault(public_key, line.split("=", 1)[1].strip())
                public_key = None
    return peers

def next_available_address(peers):
    used = {value.split("/", 1)[0] for value in peers.values()}
    for octet in range(CLIENT_START, CLIENT_END + 1):
        address = f"{WG_SUBNET_PREFIX}.{octet}"
        if address not in used:
            return f"{address}/32"
    raise RuntimeError("No available WireGuard client addresses")

def append_peer(public_key, address):
    peer_block = f"\n[Peer]\nPublicKey = {public_key}\nAllowedIPs = {address}\n"
    with WG_CONFIG.open("a", encoding="utf-8") as file:
        file.write(peer_block)

def apply_peer(public_key, address):
    subprocess.check_call(["wg", "set", WG_INTERFACE, "peer", public_key, "allowed-ips", address])

class ProvisioningHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path != "/provision":
            self.respond(404, {"error": "Not found"})
            return
        auth = self.headers.get("Authorization", "")
        header_token = self.headers.get("X-Provision-Token", "")
        if auth != f"Bearer {TOKEN}" and header_token != TOKEN:
            self.respond(401, {"error": "Unauthorized"})
            return
        try:
            length = int(self.headers.get("Content-Length", "0"))
            payload = json.loads(self.rfile.read(length) or "{}")
            public_key = str(payload.get("publicKey", "")).strip()
            validate_public_key(public_key)
            peers = current_peers()
            address = peers.get(public_key)
            created = False
            if not address:
                address = next_available_address(peers)
                apply_peer(public_key, address)
                append_peer(public_key, address)
                created = True
            self.respond(200, {
                "address": address,
                "serverPublicKey": server_public_key(),
                "endpoint": f"{SERVER_PUBLIC_IP}:{WG_PORT}",
                "dns": DNS_SERVER,
                "created": created,
            })
        except Exception as exc:
            self.respond(400, {"error": str(exc)})

    def log_message(self, format, *args):
        return

    def respond(self, status, payload):
        body = json.dumps(payload).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    ipaddress.ip_network(f"{WG_SUBNET_PREFIX}.0/24")
    server = ThreadingHTTPServer((HOST, PORT), ProvisioningHandler)
    print(f"Provisioning server listening on {HOST}:{PORT}")
    server.serve_forever()
PY

  chmod 755 "${PROVISION_INSTALL_DIR}/provisioning-server.py"

  cat > /etc/systemd/system/blockcross-provisioning.service <<EOF
[Unit]
Description=Block Cross WireGuard Provisioning
After=network.target wg-quick@${WG_INTERFACE}.service
Requires=wg-quick@${WG_INTERFACE}.service

[Service]
Type=simple
Environment=PROVISION_PORT=${PROVISION_PORT}
Environment=PROVISION_TOKEN=${PROVISION_TOKEN}
Environment=SERVER_PUBLIC_IP=${SERVER_PUBLIC_IP}
Environment=WG_INTERFACE=${WG_INTERFACE}
Environment=WG_SUBNET_PREFIX=${WG_SUBNET_PREFIX}
Environment=CLIENT_START=${CLIENT_START}
Environment=CLIENT_END=${CLIENT_END}
Environment=WG_PORT=${WG_PORT}
Environment=DNS_SERVER=${DNS_SERVER}
ExecStart=/usr/bin/python3 ${PROVISION_INSTALL_DIR}/provisioning-server.py
Restart=always
RestartSec=3
User=root

[Install]
WantedBy=multi-user.target
EOF

  systemctl daemon-reload
  systemctl enable blockcross-provisioning
  systemctl restart blockcross-provisioning
}

setup_firewall() {
  ufw allow OpenSSH || true
  ufw allow "${WG_PORT}/udp"
  ufw allow "${PROVISION_PORT}/tcp"
  ufw allow from "${WG_SUBNET}" to any port "${PRIVATE_APP_PORT}" proto tcp
  ufw deny "${PRIVATE_UPSTREAM_PUBLIC_PORT}/tcp"
  ufw deny "${PRIVATE_APP_PORT}/tcp"
  ufw --force enable

  while iptables -C FORWARD -i "${WG_INTERFACE}" -j DROP 2>/dev/null; do
    iptables -D FORWARD -i "${WG_INTERFACE}" -j DROP
  done

  iptables -C FORWARD -i "${WG_INTERFACE}" -j ACCEPT 2>/dev/null || iptables -A FORWARD -i "${WG_INTERFACE}" -j ACCEPT
  iptables -C FORWARD -o "${WG_INTERFACE}" -j ACCEPT 2>/dev/null || iptables -A FORWARD -o "${WG_INTERFACE}" -j ACCEPT
}

print_summary() {
  local server_public_key
  server_public_key="$(cat /etc/wireguard/server.pub)"

  cat <<EOF

Block Cross VPS setup complete.

WireGuard:
  Interface: ${WG_INTERFACE}
  Public endpoint: ${SERVER_PUBLIC_IP}:${WG_PORT}
  Server VPN IP: ${WG_SERVER_IP}
  Server public key: ${server_public_key}

Private app:
  URL inside VPN: http://${PRIVATE_APP_BIND_IP}:${PRIVATE_APP_PORT}
  Upstream app: ${PRIVATE_UPSTREAM_URL}
  Public upstream port blocked: ${PRIVATE_UPSTREAM_PUBLIC_PORT}/tcp
  Service: nginx

Provisioning:
  URL: http://${SERVER_PUBLIC_IP}:${PROVISION_PORT}/provision
  Token: ${PROVISION_TOKEN}
  Service: blockcross-provisioning

VPN access policy:
  Android/Windows clients route only their saved whitelist through the VPN.
  Public whitelisted sites are forwarded through the VPS.

Useful checks:
  sudo wg
  sudo systemctl status wg-quick@${WG_INTERFACE} --no-pager
  sudo systemctl status nginx --no-pager
  sudo systemctl status blockcross-provisioning --no-pager
EOF
}

main() {
  require_root
  validate_config
  install_packages
  setup_wireguard
  setup_private_app_proxy
  setup_provisioning_server
  setup_firewall
  print_summary
}

main "$@"
