#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

RELEASE_BASE_URL=${HUDSONPANEL_RELEASE_BASE_URL:-https://get.hudsonpanel.com}
LICENSE_URL=${HUDSONPANEL_LICENSE_URL:-https://hudsonpanel.com/api/license/verify.php}
INSTALL_TYPE=${HUDSONPANEL_INSTALL_TYPE:-standard}
INSTALL_CHANNEL=${HUDSONPANEL_INSTALL_CHANNEL:-release}
WORK_DIR=$(mktemp -d /tmp/hudsonpanel-installer.XXXXXX)
LOG=/var/log/hudsonpanel-install.log
exec > >(tee -a "$LOG") 2>&1
FINAL_DIR=""
FINAL_DIR_CREATED=0
INSTALL_COMPLETE=0
cleanup() {
  local status=$?
  trap - EXIT
  if (( status != 0 && INSTALL_COMPLETE == 0 && FINAL_DIR_CREATED == 1 )) && [[ -n $FINAL_DIR && $FINAL_DIR == /opt/hudsonpanel/releases/* ]]; then
    current_target=$(readlink -f /opt/hudsonpanel/current 2>/dev/null || true)
    [[ $current_target != "$FINAL_DIR" ]] || rm -f /opt/hudsonpanel/current
    rm -rf "$FINAL_DIR" "/opt/hudsonpanel/venvs/${VERSION:-invalid}"
  fi
  rm -rf "$WORK_DIR"
  exit "$status"
}
trap cleanup EXIT
fail() { printf '\nHudsonPanel cannot be installed:\n%s\n' "$*" >&2; exit 1; }
CURL_PROTOCOLS='=https'
[[ ${HUDSONPANEL_ALLOW_INSECURE_TEST_URLS:-0} == 1 ]] && CURL_PROTOCOLS='=http,https'
fetch() { curl -fsSL --proto "$CURL_PROTOCOLS" --tlsv1.2 --connect-timeout 15 --max-time "${3:-120}" "$1" -o "$2"; }
json_get() { python3 -c 'import json,sys; value=json.load(open(sys.argv[1])); [value:=value[int(p)] if isinstance(value,list) else value[p] for p in sys.argv[2].split(".")]; print("true" if value is True else "false" if value is False else value)' "$1" "$2"; }
verify_signature() { openssl pkeyutl -verify -rawin -pubin -inkey "$WORK_DIR/release-public.pem" -in "$1" -sigfile "$2" >/dev/null 2>&1 || fail "A release signature is invalid. Nothing was installed."; }
trusted_url() { [[ $1 == "${RELEASE_BASE_URL%/}/"* ]] || fail "A release manifest referenced an untrusted URL."; }

[[ $EUID -eq 0 ]] || fail "Run this installer with sudo or as root."
[[ $INSTALL_TYPE == standard || $INSTALL_TYPE == demo ]] || fail "The requested installation type is invalid."
[[ $INSTALL_CHANNEL == release || $INSTALL_CHANNEL == current || $INSTALL_CHANNEL == edge ]] || fail "The requested installation channel is invalid."
for command in curl openssl python3 sha256sum tar gzip ss; do command -v "$command" >/dev/null || fail "$command is required before HudsonPanel can be installed."; done
[[ -r /etc/os-release ]] || fail "The operating system could not be identified."
. /etc/os-release
[[ ${ID:-} == ubuntu && ${VERSION_ID:-} == 24.04 ]] || fail "HudsonPanel requires Ubuntu Server 24.04 LTS."
[[ $(uname -m) == x86_64 ]] || fail "HudsonPanel requires amd64/x86_64."
[[ $(ps -p 1 -o comm=) == systemd ]] || fail "systemd must be the active init system."
RAM_KB=$(awk '/MemTotal/{print $2}' /proc/meminfo)
(( RAM_KB >= 3800000 )) || fail "At least 4 GB of RAM is required."
DISK_KB=$(df -Pk /opt 2>/dev/null | awk 'NR==2{print $4}' || df -Pk / | awk 'NR==2{print $4}')
(( DISK_KB >= 40000000 )) || fail "At least 40 GB of free disk space is required."
for marker in /usr/local/cpanel/cpanel /usr/local/directadmin/directadmin /usr/local/psa/admin/sbin/httpdmng; do [[ ! -e $marker ]] || fail "Another hosting control panel was detected at $marker."; done
PANEL_HOSTNAME=$(hostname -f 2>/dev/null || true)
[[ $PANEL_HOSTNAME == *.* && $PANEL_HOSTNAME != localhost* ]] || fail "Set a valid fully qualified server hostname before installing."
[[ $PANEL_HOSTNAME =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]] || fail "The server hostname contains invalid characters."
for port in 80 443; do ss -H -lnt "sport = :$port" | grep -q . && fail "TCP port $port is already in use."; done

cat >"$WORK_DIR/release-public.pem" <<'EOF'
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA16QycJ+f0cRsZkAp7TDXVXhOeVcJCTmmrKRppHKj1RY=
-----END PUBLIC KEY-----
EOF

echo "Verifying this server IP with HudsonPanel..."
fetch "$LICENSE_URL" "$WORK_DIR/license.json" 30 || fail "The HudsonPanel license server could not be reached over HTTPS."
[[ $(json_get "$WORK_DIR/license.json" licensed 2>/dev/null || true) == true ]] || fail "$(json_get "$WORK_DIR/license.json" error 2>/dev/null || printf 'An active license is required to install HudsonPanel.')"
LICENSE_TYPE=$(json_get "$WORK_DIR/license.json" license_type 2>/dev/null || printf standard)
[[ $INSTALL_TYPE != demo || $LICENSE_TYPE == demo ]] || fail "This IP is not authorized as the HudsonPanel DEMO server."
LICENSED_IP=$(json_get "$WORK_DIR/license.json" ip 2>/dev/null || true)
[[ -n $LICENSED_IP ]] || fail "The license server did not return the verified public IP."
HOSTNAME_DNS_READY=0
if curl -fsSL --proto '=https' --tlsv1.2 --connect-timeout 15 --max-time 30 \
  -H 'accept: application/dns-json' \
  "https://cloudflare-dns.com/dns-query?name=${PANEL_HOSTNAME}&type=A" \
  -o "$WORK_DIR/public-dns.json"; then
  if python3 - "$WORK_DIR/public-dns.json" "$LICENSED_IP" <<'PY'
import json, sys
response = json.load(open(sys.argv[1]))
licensed_ip = sys.argv[2]
answers = response.get("Answer") or []
if response.get("Status") != 0 or not any(answer.get("type") == 1 and answer.get("data") == licensed_ip for answer in answers):
    raise SystemExit(1)
PY
  then
    HOSTNAME_DNS_READY=1
  fi
fi
if (( HOSTNAME_DNS_READY == 0 )); then
  printf 'Warning: %s does not currently resolve to %s. Installation will continue with IP access and temporary SSL.\n' "$PANEL_HOSTNAME" "$LICENSED_IP"
fi

CHANNEL=$INSTALL_CHANNEL
[[ $INSTALL_TYPE == demo ]] && CHANNEL=edge
CHANNEL_URL="${RELEASE_BASE_URL%/}/channels/${CHANNEL}.json"
fetch "$CHANNEL_URL" "$WORK_DIR/channel.json" 30 || fail "The HudsonPanel Release channel is unavailable."
fetch "${CHANNEL_URL}.sig" "$WORK_DIR/channel.json.sig" 30 || fail "The Release channel signature is unavailable."
verify_signature "$WORK_DIR/channel.json" "$WORK_DIR/channel.json.sig"
[[ $(json_get "$WORK_DIR/channel.json" schema) == 1 && $(json_get "$WORK_DIR/channel.json" product) == HudsonPanel && $(json_get "$WORK_DIR/channel.json" channel) == "$CHANNEL" ]] || fail "The ${CHANNEL^} channel identity is invalid."
VERSION=$(json_get "$WORK_DIR/channel.json" version)
CHANNEL_COMMIT=$(json_get "$WORK_DIR/channel.json" git_commit)
[[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "The published release version is malformed."
MANIFEST_URL=$(json_get "$WORK_DIR/channel.json" manifest_url)
MANIFEST_SIGNATURE_URL=$(json_get "$WORK_DIR/channel.json" manifest_signature_url)
trusted_url "$MANIFEST_URL"; trusted_url "$MANIFEST_SIGNATURE_URL"
fetch "$MANIFEST_URL" "$WORK_DIR/manifest.json" 30 || fail "The release manifest is unavailable."
fetch "$MANIFEST_SIGNATURE_URL" "$WORK_DIR/manifest.json.sig" 30 || fail "The release manifest signature is unavailable."
verify_signature "$WORK_DIR/manifest.json" "$WORK_DIR/manifest.json.sig"
[[ $(json_get "$WORK_DIR/manifest.json" schema) == 1 && $(json_get "$WORK_DIR/manifest.json" product) == HudsonPanel && $(json_get "$WORK_DIR/manifest.json" version) == "$VERSION" && $(json_get "$WORK_DIR/manifest.json" git_commit) == "$CHANNEL_COMMIT" ]] || fail "The release and channel identities do not match."
if [[ $INSTALL_TYPE == demo ]]; then
  [[ $(json_get "$WORK_DIR/manifest.json" metadata.demo_installation 2>/dev/null || true) == 1 ]] || fail "The current Edge release does not support permanent DEMO deployments yet. Publish the prepared DEMO-capable release first."
fi
PACKAGE_URL=$(json_get "$WORK_DIR/manifest.json" package.url)
PACKAGE_SIGNATURE_URL=$(json_get "$WORK_DIR/manifest.json" package.signature_url)
PACKAGE_SHA256=$(json_get "$WORK_DIR/manifest.json" package.sha256)
PACKAGE_SIZE=$(json_get "$WORK_DIR/manifest.json" package.size)
trusted_url "$PACKAGE_URL"; trusted_url "$PACKAGE_SIGNATURE_URL"

if [[ $INSTALL_TYPE == demo && -n ${HUDSONPANEL_ROOT_PASSWORD_B64:-} ]]; then
  PANEL_PASSWORD=$(printf '%s' "$HUDSONPANEL_ROOT_PASSWORD_B64" | base64 -d) || fail "The supplied DEMO panel password is invalid."
  PANEL_PASSWORD_CONFIRM=$PANEL_PASSWORD
else
  [[ -r /dev/tty ]] || fail "Run the installer from an interactive terminal to set the root panel password."
  read -r -s -p "Create the HudsonPanel root login password: " PANEL_PASSWORD </dev/tty; echo
  read -r -s -p "Confirm the password: " PANEL_PASSWORD_CONFIRM </dev/tty; echo
  [[ $PANEL_PASSWORD == "$PANEL_PASSWORD_CONFIRM" ]] || fail "The passwords did not match."
fi
(( ${#PANEL_PASSWORD} >= 10 )) || fail "The panel password must contain at least 10 characters."

ARCHIVE="$WORK_DIR/package.tar.gz"
fetch "$PACKAGE_URL" "$ARCHIVE" 1800 || fail "The release package could not be downloaded."
fetch "$PACKAGE_SIGNATURE_URL" "$WORK_DIR/package.tar.gz.sig" 30 || fail "The package signature is unavailable."
[[ $(stat -c '%s' "$ARCHIVE") == "$PACKAGE_SIZE" ]] || fail "The package size does not match its signed manifest."
verify_signature "$ARCHIVE" "$WORK_DIR/package.tar.gz.sig"
[[ $(sha256sum "$ARCHIVE" | awk '{print $1}') == "$PACKAGE_SHA256" ]] || fail "The release checksum does not match. Nothing was installed."

install -d -m 0750 /opt/hudsonpanel/releases
STAGED_DIR="/opt/hudsonpanel/releases/.staging-${VERSION}-$$"
install -d -m 0750 "$STAGED_DIR"
python3 - "$ARCHIVE" "$STAGED_DIR" "$VERSION" <<'PY'
import json, sys, tarfile
from pathlib import Path, PurePosixPath
archive_path, destination, version = Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3]
prefix = f"hudsonpanel-{version}"
with tarfile.open(archive_path, "r:gz") as archive:
    members = archive.getmembers()
    for member in members:
        path = PurePosixPath(member.name)
        if path.is_absolute() or not path.parts or path.parts[0] != prefix or ".." in path.parts or member.isdev() or member.isfifo():
            raise SystemExit("Release archive contains an unsafe path or file.")
        if member.issym() or member.islnk():
            target = PurePosixPath(member.linkname)
            if target.is_absolute() or ".." in target.parts:
                raise SystemExit("Release archive contains an unsafe link.")
    archive.extractall(destination, members=members, filter="data")
payload = destination / prefix
metadata = json.loads((payload / "RELEASE-METADATA.json").read_text())
required = [payload / "VERSION", payload / "SCHEMA_VERSION", payload / "requirements.txt", payload / "WHEELHOUSE-MANIFEST.sha256", payload / "wheelhouse", payload / "app", payload / "deploy" / "install-system.sh"]
if not all(item.exists() for item in required) or (payload / "VERSION").read_text().strip() != version or metadata.get("schema") != 1 or metadata.get("product") != "HudsonPanel" or metadata.get("version") != version:
    raise SystemExit("Release package identity is invalid.")
if int((payload / "SCHEMA_VERSION").read_text().strip()) != int(metadata["database_migrations"]["maximum"]):
    raise SystemExit("Release migration metadata is invalid.")
compatibility = metadata.get("compatibility", {})
if compatibility.get("os_id") != "ubuntu" or compatibility.get("os_version") != "24.04" or compatibility.get("architecture") != "x86_64":
    raise SystemExit("Release compatibility metadata does not match this installer.")
if list((payload / "app").rglob("*.py")) or not list((payload / "app").rglob("*.pyc")):
    raise SystemExit("Protected application payload is invalid.")
PY
PAYLOAD="$STAGED_DIR/hudsonpanel-${VERSION}"
[[ $(json_get "$PAYLOAD/RELEASE-METADATA.json" git_commit) == "$CHANNEL_COMMIT" ]] || fail "The package Git identity does not match its signed manifests."
FINAL_DIR="/opt/hudsonpanel/releases/${VERSION}"
[[ ! -e $FINAL_DIR ]] || fail "HudsonPanel $VERSION is already present on this server."
mv "$PAYLOAD" "$FINAL_DIR"
FINAL_DIR_CREATED=1
rmdir "$STAGED_DIR"
export HUDSONPANEL_VERSION="$VERSION" HUDSONPANEL_LICENSED_IP="$LICENSED_IP" HUDSONPANEL_ROOT_PASSWORD="$PANEL_PASSWORD"
export HUDSONPANEL_INSTALL_TYPE="$INSTALL_TYPE" HUDSONPANEL_INSTALL_CHANNEL="$CHANNEL"
export HUDSONPANEL_RELEASE_BASE_URL="$RELEASE_BASE_URL" HUDSONPANEL_LICENSE_URL="$LICENSE_URL"
bash "$FINAL_DIR/deploy/install-system.sh" | sed -e "s|^Open: http://.*|Open: https://${PANEL_HOSTNAME}/|"
printf 'Backup access without DNS: https://%s/\n' "$LICENSED_IP"
printf 'Emergency unencrypted access: http://%s/\n' "$LICENSED_IP"
# Releases published before HTTPS onboarding used one combined HTTP and HTTPS
# server block. Separate the listeners while preserving the hostname or IP the
# administrator entered, so direct-IP recovery never depends on working DNS.
python3 - /etc/nginx/sites-available/hudsonpanel "$PANEL_HOSTNAME" <<'PY'
import os, sys, tempfile
from pathlib import Path
path, hostname = Path(sys.argv[1]), sys.argv[2]
text = path.read_text(encoding="utf-8")
old = f"""server {{
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name {hostname} _;
"""
new = f"""server {{
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name {hostname} _;
    client_max_body_size 2G;
    include /etc/nginx/snippets/hudsonpanel-tools.conf;
    location ^~ /.well-known/acme-challenge/ {{ root /var/lib/hudsonpanel/acme; try_files $uri =404; }}
    location /static/ {{ alias /opt/hudsonpanel/current/app/static/; expires 1h; }}
    location / {{ proxy_pass http://unix:/run/hudsonpanel/hudsonpanel.sock; 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 1200s; }}
}}
server {{
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name {hostname} _;
"""
if old in text:
    text = text.replace(old, new, 1)
redirect = "    location / { return 301 https://$host$request_uri; }"
if redirect in text:
    server_header = f"    server_name {hostname} _;\n"
    http_options = server_header + "    client_max_body_size 2G;\n    include /etc/nginx/snippets/hudsonpanel-tools.conf;\n"
    text = text.replace(server_header, http_options, 1)
    proxy = "    location /static/ { alias /opt/hudsonpanel/current/app/static/; expires 1h; }\n    location / { proxy_pass http://unix:/run/hudsonpanel/hudsonpanel.sock; 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 1200s; }"
    text = text.replace(redirect, proxy, 1)
if text != path.read_text(encoding="utf-8"):
    descriptor, temporary = tempfile.mkstemp(prefix=".hudsonpanel.", dir=str(path.parent), text=True)
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8") as output:
            output.write(text)
        os.chmod(temporary, 0o644)
        os.replace(temporary, path)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)
PY
nginx -t
systemctl reload nginx
if [[ $INSTALL_TYPE == standard ]]; then
  printf '%s\n' "$CHANNEL" > /etc/hudsonpanel/update-channel
  chown root:root /etc/hudsonpanel/update-channel
  chmod 0644 /etc/hudsonpanel/update-channel
fi
INSTALL_COMPLETE=1
unset PANEL_PASSWORD PANEL_PASSWORD_CONFIRM HUDSONPANEL_ROOT_PASSWORD
