#!/usr/bin/env bash
# THEX v10.0 — Professional one-command installer
# Target: Ubuntu 22.04 LTS + OpenLiteSpeed + LSPHP 7.4
set -Eeuo pipefail
IFS=$'\n\t'

THEX_VERSION="10.0.0"
PHP_FULL="7.4"
THEX_SOURCE="https://thex.cam"
THEX_ROOT="/opt/thex"
PANEL_ROOT="/var/www/thex-panel"
PANEL_WEBROOT="${PANEL_ROOT}/public_html"
WWW_ROOT="/var/www"
VHOST_NAME="thex"
VHOST_DIR="/usr/local/lsws/conf/vhosts/${VHOST_NAME}"
VHOST_CONF="${VHOST_DIR}/vhconf.conf"
OLS_CONF="/usr/local/lsws/conf/httpd_config.conf"
PMA_VERSION="5.2.3"
PMA_DIR="/var/www/phpmyadmin"
LOG_FILE="/var/log/thex-v10-install.log"
STATE_DIR="/etc/thex"
STATE_FILE="${STATE_DIR}/install.conf"
SITE_DOMAIN="thex.cam"

exec > >(tee -a "$LOG_FILE") 2>&1

log(){ echo "[THEX] $*"; }
warn(){ echo "[THEX][WARN] $*" >&2; }
die(){ echo "[THEX][ERROR] $*" >&2; exit 1; }
trap 'rc=$?; if [[ $rc -ne 0 ]]; then echo "[THEX][ERROR] Installation failed. Log: $LOG_FILE" >&2; fi' EXIT

[[ $EUID -eq 0 ]] || die "Run this installer as root."

# Optional: curl ... | bash -s -- --domain=example.com
for arg in "$@"; do
    case "$arg" in
        --domain=*) SITE_DOMAIN="${arg#*=}" ;;
        --domain) shift; SITE_DOMAIN="${1:-}" ;;
        --help|-h)
            cat <<EOF
THEX v${THEX_VERSION}

Usage:
  curl -fsSL https://thex.cam/install | bash -s -- --domain=thex.cam

Options:
  --domain=DOMAIN   Panel domain (default: thex.cam)
EOF
            exit 0
            ;;
    esac
done
SITE_DOMAIN="${SITE_DOMAIN#https://}"
SITE_DOMAIN="${SITE_DOMAIN#http://}"
SITE_DOMAIN="${SITE_DOMAIN%/}"
[[ "$SITE_DOMAIN" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]] || die "Invalid panel domain: $SITE_DOMAIN"
export SITE_DOMAIN

source /etc/os-release
[[ "${ID:-}" == "ubuntu" && "${VERSION_ID:-}" == "22.04" ]] || die "THEX v${THEX_VERSION} requires Ubuntu 22.04 LTS."

wait_for_apt(){
    local tries=0
    while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || fuser /var/cache/apt/archives/lock >/dev/null 2>&1; do
        tries=$((tries+1)); (( tries > 120 )) && die "APT/DPKG remained locked for too long."
        log "Waiting for APT/DPKG lock..."; sleep 5
    done
}
apt_install(){ wait_for_apt; DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"; }
repair_dpkg(){ wait_for_apt; dpkg --configure -a; DEBIAN_FRONTEND=noninteractive apt-get -f install -y; }

log "============================================================"
log "THEX v${THEX_VERSION} — Professional clean installation"
log "Source: ${THEX_SOURCE}"
log "Panel domain: ${SITE_DOMAIN}"
log "============================================================"

log "Installing base packages..."
wait_for_apt
DEBIAN_FRONTEND=noninteractive apt-get update
apt_install ca-certificates curl wget unzip zip tar gzip xz-utils rsync openssl gnupg python3 python3-bcrypt python3-venv lsof psmisc procps sudo ufw fail2ban certbot

if ! dpkg -s openlitespeed >/dev/null 2>&1; then
    log "Installing OpenLiteSpeed from the official LiteSpeed repository..."
    curl -fsSL https://repo.litespeed.sh | bash
    wait_for_apt
    DEBIAN_FRONTEND=noninteractive apt-get update
    apt_install openlitespeed
fi

log "Installing LSPHP ${PHP_FULL}..."
apt_install lsphp74 lsphp74-common lsphp74-mysql lsphp74-curl lsphp74-json lsphp74-mbstring lsphp74-xml lsphp74-zip lsphp74-gd lsphp74-opcache lsphp74-intl lsphp74-imap lsphp74-bcmath lsphp74-soap lsphp74-readline || {
    warn "Some optional LSPHP extensions are unavailable; installing the required set."
    apt_install lsphp74 lsphp74-common lsphp74-mysql lsphp74-curl lsphp74-mbstring lsphp74-xml lsphp74-zip lsphp74-gd lsphp74-opcache
}
[[ -x /usr/local/lsws/lsphp74/bin/lsphp ]] || die "LSPHP 7.4 was not installed correctly."

if ! systemctl is-active --quiet mariadb; then
    log "Installing MariaDB..."
    apt_install mariadb-server mariadb-client
    systemctl enable --now mariadb
fi

if ! systemctl is-active --quiet redis-server 2>/dev/null; then
    apt_install redis-server
    systemctl enable --now redis-server || warn "Redis could not be started; continuing."
fi
systemctl enable --now fail2ban || warn "Fail2Ban could not be started; continuing."

# Download THEX only from the user's own domain.
APP_URL="${THEX_SOURCE}/release/v10/thex-v10.tar.gz"
SUMS_URL="${THEX_SOURCE}/release/v10/SHA256SUMS"
TMP_APP="/tmp/thex-v10.tar.gz"
TMP_SUMS="/tmp/thex-v10.SHA256SUMS"

log "Downloading THEX application from ${THEX_SOURCE}..."
curl --fail --location --retry 5 --retry-delay 2 --connect-timeout 15 --max-time 600 -o "$TMP_APP" "$APP_URL" || die "Could not download THEX package from ${APP_URL}"
curl --fail --location --retry 5 --retry-delay 2 --connect-timeout 15 --max-time 60 -o "$TMP_SUMS" "$SUMS_URL" || die "Could not download SHA256SUMS from ${SUMS_URL}"
EXPECTED="$(awk '$2 ~ /thex-v10\.tar\.gz$/ {print $1; exit}' "$TMP_SUMS")"
[[ "$EXPECTED" =~ ^[A-Fa-f0-9]{64}$ ]] || die "Invalid SHA256 manifest."
ACTUAL="$(sha256sum "$TMP_APP" | awk '{print $1}')"
[[ "$ACTUAL" == "$EXPECTED" ]] || die "THEX package SHA256 mismatch."

rm -rf "${THEX_ROOT}.new"
mkdir -p "${THEX_ROOT}.new"
tar -xzf "$TMP_APP" -C "${THEX_ROOT}.new" --strip-components=1
[[ -f "${THEX_ROOT}.new/panel/index.php" && -f "${THEX_ROOT}.new/agent/agent.py" ]] || die "THEX archive failed package validation."
rm -rf "$THEX_ROOT"
mv "${THEX_ROOT}.new" "$THEX_ROOT"
rm -f "$TMP_APP" "$TMP_SUMS"

# Fix the exact Python failure from v9.1 without touching files that already import os.
while IFS= read -r -d '' py; do
    if grep -Eq '(^|[^[:alnum:]_])os\.' "$py" && ! grep -Eq '^[[:space:]]*(import[[:space:]]+os([[:space:]]|$)|from[[:space:]]+os[[:space:]]+import[[:space:]])' "$py"; then
        sed -i '1i import os' "$py"
    fi
done < <(find "$THEX_ROOT" -type f -name '*.py' -print0)

log "Preparing panel files..."
rm -rf "$PANEL_ROOT"
mkdir -p "$PANEL_WEBROOT"
rsync -a --delete "$THEX_ROOT/panel/" "$PANEL_WEBROOT/"
cat > "$PANEL_WEBROOT/index.php" <<'PHP'
<?php
header('Location: /login.php', true, 302);
exit;
PHP

mkdir -p "$PMA_DIR"

# phpMyAdmin 5.2.3 is compatible with PHP 7.2+ according to its official release page.
# Prefer the self-hosted copy on thex.cam; fall back to the official release only if the
# administrator has not uploaded the archive to thex.cam yet.
PMA_LOCAL_URL="${THEX_SOURCE}/release/v10/phpMyAdmin-${PMA_VERSION}-all-languages.tar.gz"
PMA_OFFICIAL_URL="https://files.phpmyadmin.net/phpMyAdmin/${PMA_VERSION}/phpMyAdmin-${PMA_VERSION}-all-languages.tar.gz"
PMA_TMP="/tmp/phpMyAdmin-${PMA_VERSION}.tar.gz"
if curl --fail --location --retry 3 --connect-timeout 10 --max-time 600 -o "$PMA_TMP" "$PMA_LOCAL_URL"; then
    log "phpMyAdmin ${PMA_VERSION}: using self-hosted THEX copy."
else
    warn "Self-hosted phpMyAdmin archive is not present on thex.cam; using the official phpMyAdmin release endpoint."
    curl --fail --location --retry 5 --retry-delay 2 --connect-timeout 15 --max-time 600 -o "$PMA_TMP" "$PMA_OFFICIAL_URL" || die "phpMyAdmin ${PMA_VERSION} download failed."
fi
rm -rf /tmp/phpMyAdmin-${PMA_VERSION}-all-languages "$PMA_DIR"
tar -xzf "$PMA_TMP" -C /tmp || die "Invalid phpMyAdmin archive."
mv "/tmp/phpMyAdmin-${PMA_VERSION}-all-languages" "$PMA_DIR"
rm -f "$PMA_TMP"
mkdir -p "$PMA_DIR/tmp"
chmod 700 "$PMA_DIR/tmp"

# phpMyAdmin needs a Blowfish secret for cookie auth.
BLOWFISH="$(openssl rand -hex 32)"
cat > "$PMA_DIR/config.inc.php" <<PHP
<?php
\$cfg['blowfish_secret'] = '${BLOWFISH}';
\$cfg['Servers'][1]['auth_type'] = 'cookie';
\$cfg['Servers'][1]['host'] = 'localhost';
\$cfg['Servers'][1]['compress'] = false;
\$cfg['TempDir'] = '/var/www/phpmyadmin/tmp';
PHP

# Secure panel configuration with a random secret and random initial admin password.
mkdir -p "$STATE_DIR"
SECRET="$(openssl rand -hex 32)"
ADMIN_PASS="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9@#%+=_' | head -c 20)"
[[ ${#ADMIN_PASS} -ge 16 ]] || ADMIN_PASS="THEX-$(openssl rand -hex 12)"
ADMIN_HASH="$(THEX_PASS="$ADMIN_PASS" python3 - <<'PY'
import os,bcrypt
print(bcrypt.hashpw(os.environ['THEX_PASS'].encode(),bcrypt.gensalt(rounds=12)).decode())
PY
)"
cat > "$PANEL_WEBROOT/config.php" <<PHP
<?php
return [
 'secret' => '${SECRET}',
 'admin_user' => 'admin',
 'admin_password_hash' => '${ADMIN_HASH}',
];
PHP
chmod 640 "$PANEL_WEBROOT/config.php"

# OLS vhost: dedicated THEX vhost, no Example realm and no protected-context dependency.
log "Configuring OpenLiteSpeed..."
mkdir -p "$VHOST_DIR" "$PANEL_WEBROOT/logs"
cat > "$VHOST_CONF" <<EOF
vhRoot                   ${PANEL_ROOT}/
docRoot                  ${PANEL_WEBROOT}/
vhDomain                 ${SITE_DOMAIN}
vhAliases                www.${SITE_DOMAIN}
adminEmails              root@localhost
enableGzip               1

index {
    useServer             0
    indexFiles            index.php,index.html,index.htm
}

errorPage 404 {
    url                   /error404.html
}

context / {
    allowBrowse           0
    addDefaultCharset     off
}

context /phpmyadmin/ {
    location              ${PMA_DIR}/
    allowBrowse           0
}

rewrite {
    enable                1
    autoLoadHtaccess      1
}

accessLog ${PANEL_WEBROOT}/logs/access.log {
    useServer             0
    rollingSize           10M
    keepDays              7
}

errorLog ${PANEL_WEBROOT}/logs/error.log {
    useServer             0
    logLevel              WARN
    rollingSize           10M
    keepDays              7
}
EOF

# Use Python only to edit structured OLS blocks; never replace the whole config blindly.
SITE_DOMAIN="$SITE_DOMAIN" python3 - <<'PY'
from pathlib import Path
import os,re,shutil

conf=Path('/usr/local/lsws/conf/httpd_config.conf')
if not conf.exists():
    raise SystemExit('OpenLiteSpeed httpd_config.conf not found')
s=conf.read_text(errors='ignore')
Path(str(conf)+'.thex-pre-v10').write_text(s)

def listener_block(text,name):
    m=re.search(r'(?ms)^\s*listener\s+'+re.escape(name)+r'\s*\{.*?^\s*\}',text)
    return m

# Default listener on port 80.
m=listener_block(s,'Default')
if not m:
    s += '\nlistener Default {\n    address *:80\n    secure 0\n}\n'
    m=listener_block(s,'Default')
else:
    block=m.group(0)
    block=re.sub(r'(?m)^\s*address\s+[^\n]+$', '    address *:80', block)
    if not re.search(r'(?m)^\s*address\s+\*:80\s*$',block):
        block=block.replace('{','{\n    address *:80',1)
    s=s[:m.start()]+block+s[m.end():]
    m=listener_block(s,'Default')

# Add THEX domain mappings inside the listener, not after it.
m=listener_block(s,'Default')
block=m.group(0)
for d in (os.environ['SITE_DOMAIN'], 'www.'+os.environ['SITE_DOMAIN']):
    if not re.search(r'(?m)^\s*map\s+thex\s+'+re.escape(d)+r'\s*$',block):
        block=block.rstrip()[:-1] + f'\n    map thex {d}\n}}'
s=s[:m.start()]+block+s[m.end():]

# Server-level PHP LSAPI, matching LiteSpeed's documented configuration pattern.
if not re.search(r'(?ms)^\s*extprocessor\s+lsphp\s*\{',s):
    s += '''\nextprocessor lsphp {
    type                    lsapi
    address                 uds://tmp/lshttpd/lsphp.sock
    maxConns                10
    env                     PHP_LSAPI_CHILDREN=10
    env                     LSAPI_AVOID_FORK=200M
    initTimeout             60
    retryTimeout            0
    persistConn             1
    respBuffer              0
    autoStart               2
    path                    /usr/local/lsws/lsphp74/bin/lsphp
    backlog                 100
    instances               1
}
'''
if not re.search(r'(?ms)^\s*scripthandler\s*\{',s):
    s += '''\nscripthandler {
    add                     lsapi:lsphp php
}
'''

# Replace only an existing stale THEX vhost block, then add the clean one.
s=re.sub(r'(?ms)^\s*virtualhost\s+thex\s*\{.*?^\s*\}\s*','',s)
s += '''\nvirtualhost thex {
    vhRoot                  /var/www/thex-panel/
    configFile              /usr/local/lsws/conf/vhosts/thex/vhconf.conf
    allowSymbolLink         1
    enableScript            1
    restrained              1
    setUIDMode              0
}
'''
conf.write_text(s)
PY

# Put the panel user in the agent group so PHP can access the UNIX socket without making it world-writable.
id thex >/dev/null 2>&1 || useradd --system --home /opt/thex --shell /usr/sbin/nologin thex
usermod -aG thex lsadm 2>/dev/null || true
mkdir -p /run/thex
cp -f "$THEX_ROOT/agent/agent.py" /opt/thex/agent.py
chown root:root /opt/thex/agent.py
chmod 750 /opt/thex/agent.py
cat > /etc/systemd/system/thex-agent.service <<'EOF'
[Unit]
Description=THEX Privileged Agent
After=network-online.target mariadb.service redis-server.service
Wants=network-online.target

[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/bin/python3 /opt/thex/agent.py
Restart=always
RestartSec=2
RuntimeDirectory=thex
RuntimeDirectoryMode=0770
ProtectSystem=full
ProtectHome=false
ReadWritePaths=/var/www /opt/thex /usr/local/lsws /etc/letsencrypt /etc/ufw /etc/fail2ban /run/thex

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now thex-agent

# Permissions: panel is served by OLS; config is not writable by the web user.
chown -R lsadm:lsadm "$PANEL_ROOT" "$PMA_DIR"
find "$PANEL_ROOT" -type d -exec chmod 755 {} +
find "$PANEL_ROOT" -type f -exec chmod 644 {} +
chmod 640 "$PANEL_WEBROOT/config.php"
find "$PMA_DIR" -type d -exec chmod 755 {} +
find "$PMA_DIR" -type f -exec chmod 644 {} +
chmod 700 "$PMA_DIR/tmp"

# PHP production defaults.
PHP_INI="/usr/local/lsws/lsphp74/etc/php/7.4/litespeed/php.ini"
[[ -f "$PHP_INI" ]] || PHP_INI="/usr/local/lsws/lsphp74/etc/php/7.4/php.ini"
if [[ -f "$PHP_INI" ]]; then
    sed -i -e 's/^;*memory_limit.*/memory_limit = 256M/' -e 's/^;*upload_max_filesize.*/upload_max_filesize = 64M/' -e 's/^;*post_max_size.*/post_max_size = 64M/' -e 's/^;*max_execution_time.*/max_execution_time = 120/' "$PHP_INI" || true
fi

# Syntax validation before touching the service.
log "Validating Python and PHP files..."
while IFS= read -r -d '' py; do python3 -m py_compile "$py"; done < <(find "$THEX_ROOT" -type f -name '*.py' -print0)
/usr/local/lsws/bin/lshttpd -t

# Enable HTTP/HTTPS. HTTPS is attempted only when DNS already points to this server.
ufw allow 22/tcp >/dev/null || true
ufw allow 80/tcp >/dev/null || true
ufw allow 443/tcp >/dev/null || true
ufw --force enable >/dev/null || true

# Store install state and credentials for the administrator.
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
cat > "$STATE_FILE" <<EOF
THEX_VERSION=${THEX_VERSION}
THEX_SOURCE=${THEX_SOURCE}
SITE_DOMAIN=${SITE_DOMAIN}
PANEL_URL=http://${SITE_DOMAIN}/
PHP_VERSION=${PHP_FULL}
PMA_VERSION=${PMA_VERSION}
PANEL_ROOT=${PANEL_ROOT}
PANEL_WEBROOT=${PANEL_WEBROOT}
VHOST_CONFIG=${VHOST_CONF}
EOF
chmod 600 "$STATE_FILE"
cat > "${STATE_DIR}/credentials.txt" <<EOF
THEX v${THEX_VERSION} initial credentials
Panel: http://${SITE_DOMAIN}/
Username: admin
Password: ${ADMIN_PASS}

Change the password after first login.
EOF
chmod 600 "${STATE_DIR}/credentials.txt"

# Final service restart and health checks.
log "Restarting OpenLiteSpeed..."
if systemctl list-unit-files | grep -q '^lsws\.service'; then
    systemctl enable lsws
    systemctl restart lsws
else
    die "lsws.service was not found."
fi
systemctl is-active --quiet lsws || die "OpenLiteSpeed is not active."
systemctl is-active --quiet mariadb || die "MariaDB is not active."
systemctl is-active --quiet thex-agent || die "THEX Agent is not active."

# Local HTTP health check. Host header verifies the vhost mapping.
log "Running local HTTP health check..."
if curl -fsS --max-time 15 -H "Host: ${SITE_DOMAIN}" http://127.0.0.1/login.php >/tmp/thex-health.html; then
    grep -q "THEX Login" /tmp/thex-health.html || die "THEX login page did not render correctly."
else
    die "Local HTTP health check failed."
fi
rm -f /tmp/thex-health.html

log "============================================================"
log "THEX v${THEX_VERSION} installed successfully."
log "Panel: http://${SITE_DOMAIN}/"
log "Username: admin"
log "Password saved securely in: ${STATE_DIR}/credentials.txt"
log "Install state: ${STATE_FILE}"
log "Installer log: ${LOG_FILE}"
log "THEX downloads: ${THEX_SOURCE}"
log "============================================================"
trap - EXIT
