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

if [ "$(id -u)" -ne 0 ]; then
  echo "Run this installer over SSH as root, or with sudo." >&2
  exit 1
fi

XYROPANEL_USER="${XYROPANEL_USER:-xyropanel}"
XYROPANEL_HOME="${XYROPANEL_HOME:-/opt/xyropanel}"
XYROPANEL_PORT="${XYROPANEL_PORT:-3001}"
XYROPANEL_HTTP_PORT="${XYROPANEL_HTTP_PORT:-8686}"
XYROPANEL_HTTPS_PORT="${XYROPANEL_HTTPS_PORT:-8688}"
XYROPANEL_DOMAIN="${XYROPANEL_DOMAIN:-}"
XYROPANEL_EMAIL="${XYROPANEL_EMAIL:-}"
XYROPANEL_AUTO_HOSTNAME="${XYROPANEL_AUTO_HOSTNAME:-1}"
XYROPANEL_REF="${XYROPANEL_REF:-main}"
XYROPANEL_RELEASE_BASE_URL="${XYROPANEL_RELEASE_BASE_URL:-https://install.ashtek.net/releases}"
XYROPANEL_SOURCE_URL="${XYROPANEL_SOURCE_URL:-$XYROPANEL_RELEASE_BASE_URL/xyropanel-latest.tar.gz}"
XYROPANEL_GIT_URL="${XYROPANEL_GIT_URL:-}"
XYROPANEL_DEMO="${XYROPANEL_DEMO:-0}"
XYROPANEL_INSTALL_AGENT="${XYROPANEL_INSTALL_AGENT:-1}"
XYROPANEL_INSTALL_STACK="${XYROPANEL_INSTALL_STACK:-1}"
XYROPANEL_INSTALL_MAIL="${XYROPANEL_INSTALL_MAIL:-1}"
XYROPANEL_INSTALL_POSTGRESQL="${XYROPANEL_INSTALL_POSTGRESQL:-1}"
XYROPANEL_BACKUP_BEFORE_UPGRADE="${XYROPANEL_BACKUP_BEFORE_UPGRADE:-1}"

XYROPANEL_APP_DIR="$XYROPANEL_HOME/app"
XYROPANEL_RELEASE_DIR="$XYROPANEL_HOME/releases"
XYROPANEL_BACKUP_DIR="$XYROPANEL_HOME/backups"
XYROPANEL_ACME_DIR="/var/www/xyropanel-acme"
XYROPANEL_TLS_DIR="/etc/xyropanel/tls"
XYROPANEL_INSTALL_LOG="/var/log/xyropanel-install.log"
INSTALL_STARTED_AT="$(date +%Y%m%d%H%M%S)"
LAST_BACKUP_ARCHIVE=""
XYROPANEL_DOMAIN_AUTO_DETECTED="0"

if ! command -v systemctl >/dev/null 2>&1; then
  echo "xyroPanel requires a systemd-based Linux server." >&2
  exit 1
fi

if [ ! -r /etc/os-release ]; then
  echo "Could not detect Linux distribution." >&2
  exit 1
fi

# shellcheck disable=SC1091
. /etc/os-release
DISTRO_ID="$ID"
case "$DISTRO_ID" in
  ubuntu|debian|almalinux|rocky) ;;
  rhel) DISTRO_ID="rocky" ;;
  *) echo "Unsupported distro: $DISTRO_ID" >&2; exit 1 ;;
esac

detect_panel_hostname() {
  if [ -n "$XYROPANEL_DOMAIN" ] || [ "$XYROPANEL_AUTO_HOSTNAME" != "1" ]; then
    return
  fi

  local candidate=""
  candidate="$(hostname -f 2>/dev/null || hostname 2>/dev/null || true)"
  candidate="${candidate%.}"
  case "$candidate" in
    ""|localhost|localhost.localdomain|*.local|*.localdomain)
      return
      ;;
  esac

  if [[ "$candidate" == *.* ]]; then
    XYROPANEL_DOMAIN="$candidate"
    XYROPANEL_DOMAIN_AUTO_DETECTED="1"
  fi
}

detect_panel_hostname
if [ -z "$XYROPANEL_EMAIL" ]; then
  XYROPANEL_EMAIL="admin@${XYROPANEL_DOMAIN:-$HOSTNAME}"
fi

log() {
  printf '\n\033[1;36m==> %s\033[0m\n' "$*"
}

warn() {
  printf '\n\033[1;33mWARN: %s\033[0m\n' "$*" >&2
}

fail() {
  printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2
  if [ -n "$LAST_BACKUP_ARCHIVE" ]; then
    cat >&2 <<MSG

A pre-upgrade backup was created:
  $LAST_BACKUP_ARCHIVE

Rollback helper:
  $XYROPANEL_HOME/rollback-last.sh

MSG
  fi
  exit 1
}

trap 'fail "Installation failed. Check $XYROPANEL_INSTALL_LOG for details."' ERR

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

write_rollback_helper() {
  local archive="$1"
  [ -n "$archive" ] || return 0
  cat > "$XYROPANEL_HOME/rollback-last.sh" <<SCRIPT
#!/usr/bin/env bash
set -euo pipefail
if [ "\$(id -u)" -ne 0 ]; then
  echo "Run rollback as root, or with sudo." >&2
  exit 1
fi
systemctl stop xyropanel.service >/dev/null 2>&1 || true
if [ -d "$XYROPANEL_APP_DIR" ]; then
  mv "$XYROPANEL_APP_DIR" "$XYROPANEL_APP_DIR.failed-\$(date +%Y%m%d%H%M%S)"
fi
tar -xzf "$archive" -C "$XYROPANEL_HOME"
chown -R "$XYROPANEL_USER:$XYROPANEL_USER" "$XYROPANEL_APP_DIR"
systemctl daemon-reload
systemctl start xyropanel.service
echo "xyroPanel rolled back from $archive"
SCRIPT
  chmod 0750 "$XYROPANEL_HOME/rollback-last.sh"
}

backup_existing_install() {
  if [ "$XYROPANEL_BACKUP_BEFORE_UPGRADE" != "1" ] || [ ! -d "$XYROPANEL_APP_DIR" ]; then
    return
  fi
  install -d -m 0750 "$XYROPANEL_BACKUP_DIR"
  LAST_BACKUP_ARCHIVE="$XYROPANEL_BACKUP_DIR/pre-upgrade-$INSTALL_STARTED_AT.tar.gz"
  log "Creating pre-upgrade backup"
  tar \
    --exclude="$XYROPANEL_APP_DIR/node_modules" \
    --exclude="$XYROPANEL_APP_DIR/.next/cache" \
    -czf "$LAST_BACKUP_ARCHIVE" \
    -C "$XYROPANEL_HOME" app
  chmod 0600 "$LAST_BACKUP_ARCHIVE"
  write_rollback_helper "$LAST_BACKUP_ARCHIVE"
}

set_env_var() {
  local file="$1"
  local key="$2"
  local value="$3"
  ENV_FILE="$file" ENV_KEY="$key" ENV_VALUE="$value" python3 - <<'PY'
import os
from pathlib import Path

path = Path(os.environ["ENV_FILE"])
key = os.environ["ENV_KEY"]
value = os.environ["ENV_VALUE"]
line = f"{key}={value}"
lines = path.read_text().splitlines() if path.exists() else []
for index, existing in enumerate(lines):
    if existing.startswith(f"{key}="):
        lines[index] = line
        break
else:
    lines.append(line)
path.write_text("\n".join(lines) + "\n")
PY
}

configure_root_mail_alias() {
  if [ ! -f /etc/aliases ] || grep -Eq '^root:' /etc/aliases; then
    return
  fi

  printf '\nroot: %s\n' "$XYROPANEL_EMAIL" >> /etc/aliases
  if command -v newaliases >/dev/null 2>&1; then
    newaliases || true
  fi
}

install_base_packages_debian() {
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qq
  apt-get install -y ca-certificates curl git gnupg nginx openssl python3 rsync sqlite3 tar unzip

  if ! command -v node >/dev/null 2>&1 || ! node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)' >/dev/null 2>&1; then
    curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
    apt-get install -y nodejs
  fi
}

install_base_packages_rpm() {
  dnf install -y ca-certificates curl git nginx openssl python3 rsync sqlite tar unzip
  if ! command -v node >/dev/null 2>&1 || ! node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)' >/dev/null 2>&1; then
    dnf module reset -y nodejs || true
    dnf module enable -y nodejs:20 || true
    dnf install -y nodejs npm
  fi
}

package_available_debian() {
  apt-cache show "$1" >/dev/null 2>&1
}

append_docker_packages_debian() {
  local -n package_ref="$1"
  package_ref+=(docker.io)

  if package_available_debian docker-compose-plugin; then
    package_ref+=(docker-compose-plugin)
  elif package_available_debian docker-compose-v2; then
    package_ref+=(docker-compose-v2)
  elif package_available_debian docker-compose; then
    package_ref+=(docker-compose)
  else
    warn "No Docker Compose package was found in configured apt repositories. Docker will be installed without Compose."
  fi
}

install_hosting_stack_debian() {
  export DEBIAN_FRONTEND=noninteractive
  local packages=(certbot python3-certbot-nginx mariadb-server redis-server php-fpm php-cli python3-venv python3-pip python3-gunicorn uvicorn)
  append_docker_packages_debian packages
  if [ "$XYROPANEL_INSTALL_POSTGRESQL" = "1" ]; then
    packages+=(postgresql postgresql-client)
  fi
  if [ "$XYROPANEL_INSTALL_MAIL" = "1" ]; then
    configure_root_mail_alias
    echo "postfix postfix/mailname string ${XYROPANEL_DOMAIN:-$HOSTNAME}" | debconf-set-selections || true
    echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections || true
    packages+=(postfix dovecot-core dovecot-imapd dovecot-lmtpd dovecot-sieve dovecot-managesieved clamav clamav-daemon clamav-freshclam rspamd)
  fi
  apt-get install -y "${packages[@]}"
}

install_hosting_stack_rpm() {
  dnf install -y epel-release || true
  local packages=(certbot python3-certbot-nginx mariadb-server redis php-fpm php-cli python3-pip python3-gunicorn python3-uvicorn docker)
  if [ "$XYROPANEL_INSTALL_POSTGRESQL" = "1" ]; then
    packages+=(postgresql-server postgresql)
  fi
  if [ "$XYROPANEL_INSTALL_MAIL" = "1" ]; then
    packages+=(postfix dovecot dovecot-pigeonhole clamav clamav-update clamd rspamd)
  fi
  dnf install -y "${packages[@]}"
  dnf install -y docker-compose-plugin || dnf install -y docker-compose || warn "No Docker Compose package was found in configured dnf repositories. Docker will be installed without Compose."
  if [ "$XYROPANEL_INSTALL_POSTGRESQL" = "1" ] && [ ! -f /var/lib/pgsql/data/PG_VERSION ]; then
    postgresql-setup --initdb || true
  fi
}

refresh_clamav_definitions() {
  command -v freshclam >/dev/null 2>&1 || return

  systemctl stop clamav-freshclam freshclam >/dev/null 2>&1 || true
  if ! freshclam --quiet; then
    log "ClamAV definitions were not refreshed during install; the freshclam service will retry automatically."
  fi
}

ensure_vmail_user() {
  if id vmail >/dev/null 2>&1; then
    return
  fi

  if getent group 5000 >/dev/null 2>&1; then
    useradd --home-dir /var/vmail --shell /usr/sbin/nologin --uid 5000 vmail || useradd --home-dir /var/vmail --shell /sbin/nologin vmail || true
  else
    groupadd --gid 5000 vmail >/dev/null 2>&1 || true
    useradd --home-dir /var/vmail --shell /usr/sbin/nologin --uid 5000 --gid 5000 vmail || useradd --home-dir /var/vmail --shell /sbin/nologin --uid 5000 --gid 5000 vmail || true
  fi
}

enable_stack_services() {
  install -d -m 0755 /var/www/xyropanel
  install -d -m 0750 /var/backups/xyropanel/account-backups /var/backups/xyropanel/server-backups /var/backups/xyropanel/agency-backups /var/backups/xyropanel/imports
  install -d -m 0750 /etc/xyropanel/database-credentials /etc/xyropanel/mailboxes

  systemctl enable --now nginx || true
  systemctl enable --now mariadb || systemctl enable --now mysql || true
  systemctl enable --now redis-server || systemctl enable --now redis || true
  systemctl enable --now docker || true
  systemctl enable --now php8.3-fpm || systemctl enable --now php-fpm || true
  if [ "$XYROPANEL_INSTALL_POSTGRESQL" = "1" ]; then
    systemctl enable --now postgresql || true
  fi
  if [ "$XYROPANEL_INSTALL_MAIL" = "1" ]; then
    ensure_vmail_user
    install -d -o vmail -g vmail -m 0750 /var/vmail
    touch /etc/xyropanel/postfix-virtual-mailboxes /etc/xyropanel/postfix-virtual-aliases /etc/xyropanel/dovecot-users
    chmod 0600 /etc/xyropanel/postfix-virtual-mailboxes /etc/xyropanel/postfix-virtual-aliases /etc/xyropanel/dovecot-users
    postmap /etc/xyropanel/postfix-virtual-mailboxes || true
    postmap /etc/xyropanel/postfix-virtual-aliases || true
    postconf -e "virtual_mailbox_domains = proxy:regexp:/etc/xyropanel/postfix-virtual-domains.regexp" || true
    printf '/.+/ OK\n' > /etc/xyropanel/postfix-virtual-domains.regexp
    postconf -e "virtual_mailbox_base = /var/vmail" || true
    postconf -e "virtual_mailbox_maps = hash:/etc/xyropanel/postfix-virtual-mailboxes" || true
    postconf -e "virtual_alias_maps = hash:/etc/xyropanel/postfix-virtual-aliases" || true
    postconf -e "virtual_uid_maps = static:5000" || true
    postconf -e "virtual_gid_maps = static:5000" || true
    postconf -e "smtpd_milters = inet:127.0.0.1:11332" || true
    postconf -e "non_smtpd_milters = inet:127.0.0.1:11332" || true
    postconf -e "milter_protocol = 6" || true
    postconf -e "milter_default_action = accept" || true
    install -d -m 0755 /etc/dovecot/conf.d
    cat >/etc/dovecot/conf.d/99-xyropanel-mail.conf <<'MAILCONF'
mail_location = maildir:/var/vmail/%d/%n
passdb {
  driver = passwd-file
  args = username_format=%u /etc/xyropanel/dovecot-users
}
userdb {
  driver = passwd-file
  args = username_format=%u /etc/xyropanel/dovecot-users
}
service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}
MAILCONF
    install -d -m 0755 /etc/rspamd/local.d
    cat >/etc/rspamd/local.d/antivirus.conf <<'AVCONF'
clamav {
  type = "clamav";
  servers = "/var/run/clamav/clamd.ctl";
  symbol = "CLAM_VIRUS";
  action = "reject";
  message = "Virus found: %VIRUS%";
}
AVCONF
    cat >/etc/rspamd/local.d/milter_headers.conf <<'MILTERCONF'
extended_spam_headers = true;
MILTERCONF
    configure_root_mail_alias
    refresh_clamav_definitions
    systemctl enable --now clamav-freshclam || systemctl enable --now freshclam || true
    systemctl enable --now clamav-daemon || systemctl enable --now clamd@scan || systemctl enable --now clamd || true
    systemctl enable --now rspamd || true
    systemctl enable --now postfix || true
    systemctl enable --now dovecot || true
  fi
}

create_user() {
  if ! id "$XYROPANEL_USER" >/dev/null 2>&1; then
    useradd --system --create-home --home-dir "$XYROPANEL_HOME" --shell /usr/sbin/nologin "$XYROPANEL_USER"
  fi
  install -d -o "$XYROPANEL_USER" -g "$XYROPANEL_USER" -m 0755 "$XYROPANEL_HOME" "$XYROPANEL_RELEASE_DIR"
  install -d -o "$XYROPANEL_USER" -g "$XYROPANEL_USER" -m 0750 "$XYROPANEL_BACKUP_DIR"
}

install_source() {
  local stage="$XYROPANEL_RELEASE_DIR/source-$INSTALL_STARTED_AT"
  rm -rf "$stage"
  install -d -o "$XYROPANEL_USER" -g "$XYROPANEL_USER" -m 0755 "$stage"

  if [ -n "$XYROPANEL_SOURCE_URL" ]; then
    log "Downloading xyroPanel release archive from $XYROPANEL_SOURCE_URL"
    curl -fsSL "$XYROPANEL_SOURCE_URL" | tar -xz --strip-components=1 -C "$stage"
  elif [ -n "$XYROPANEL_GIT_URL" ]; then
    log "Cloning xyroPanel source from $XYROPANEL_GIT_URL"
    git clone --depth 1 --branch "$XYROPANEL_REF" "$XYROPANEL_GIT_URL" "$stage"
  else
    fail "No xyroPanel source is configured. Set XYROPANEL_SOURCE_URL or XYROPANEL_GIT_URL."
  fi

  install -d -o "$XYROPANEL_USER" -g "$XYROPANEL_USER" -m 0755 "$XYROPANEL_APP_DIR"
  rsync -a --delete \
    --exclude data \
    --exclude .env \
    --exclude node_modules \
    --exclude .next/cache \
    "$stage/" "$XYROPANEL_APP_DIR/"
  chown -R "$XYROPANEL_USER:$XYROPANEL_USER" "$XYROPANEL_APP_DIR" "$stage"
}

configure_app() {
  export NO_UPDATE_NOTIFIER=1
  export NPM_CONFIG_AUDIT=false
  export NPM_CONFIG_FUND=false
  export NPM_CONFIG_UPDATE_NOTIFIER=false

  install -d -o "$XYROPANEL_USER" -g "$XYROPANEL_USER" -m 0750 "$XYROPANEL_APP_DIR/data"
  if [ ! -f "$XYROPANEL_APP_DIR/data/secrets.key" ]; then
    openssl rand -base64 32 > "$XYROPANEL_APP_DIR/data/secrets.key"
    chown "$XYROPANEL_USER:$XYROPANEL_USER" "$XYROPANEL_APP_DIR/data/secrets.key"
    chmod 0600 "$XYROPANEL_APP_DIR/data/secrets.key"
  fi

  touch "$XYROPANEL_APP_DIR/.env"
  local public_url=""
  if [ -n "$XYROPANEL_DOMAIN" ]; then
    public_url="https://$XYROPANEL_DOMAIN:$XYROPANEL_HTTPS_PORT"
  fi
  set_env_var "$XYROPANEL_APP_DIR/.env" "NODE_ENV" "production"
  set_env_var "$XYROPANEL_APP_DIR/.env" "PORT" "$XYROPANEL_PORT"
  set_env_var "$XYROPANEL_APP_DIR/.env" "NEXT_PUBLIC_APP_URL" "$public_url"
  set_env_var "$XYROPANEL_APP_DIR/.env" "XYROPANEL_FIRST_RUN" "1"
  set_env_var "$XYROPANEL_APP_DIR/.env" "XYROPANEL_DEMO" "$XYROPANEL_DEMO"
  if ! grep -q '^XYROPANEL_MAINTENANCE_TOKEN=' "$XYROPANEL_APP_DIR/.env" 2>/dev/null; then
    set_env_var "$XYROPANEL_APP_DIR/.env" "XYROPANEL_MAINTENANCE_TOKEN" "$(openssl rand -hex 32)"
  fi
  chown "$XYROPANEL_USER:$XYROPANEL_USER" "$XYROPANEL_APP_DIR/.env"
  chmod 0600 "$XYROPANEL_APP_DIR/.env"

  sudo -u "$XYROPANEL_USER" env NO_UPDATE_NOTIFIER=1 NPM_CONFIG_AUDIT=false NPM_CONFIG_FUND=false NPM_CONFIG_UPDATE_NOTIFIER=false bash -lc "cd '$XYROPANEL_APP_DIR' && npm ci --no-audit --no-fund --loglevel=error && npm run build --loglevel=error"
  if [ "$XYROPANEL_DEMO" = "1" ]; then
    sudo -u "$XYROPANEL_USER" env NO_UPDATE_NOTIFIER=1 NPM_CONFIG_AUDIT=false NPM_CONFIG_FUND=false NPM_CONFIG_UPDATE_NOTIFIER=false bash -lc "cd '$XYROPANEL_APP_DIR' && npm run db:demo --loglevel=error"
  fi
}

write_service() {
  cat > /etc/systemd/system/xyropanel.service <<UNIT
[Unit]
Description=xyroPanel control panel
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$XYROPANEL_USER
Group=$XYROPANEL_USER
WorkingDirectory=$XYROPANEL_HOME/app
EnvironmentFile=$XYROPANEL_HOME/app/.env
Environment=NPM_CONFIG_UPDATE_NOTIFIER=false
Environment=NPM_CONFIG_AUDIT=false
Environment=NPM_CONFIG_FUND=false
Environment=NO_UPDATE_NOTIFIER=1
ExecStart=/usr/bin/npm run start --silent -- --hostname 127.0.0.1 --port $XYROPANEL_PORT
Restart=always
RestartSec=10
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
ReadWritePaths=$XYROPANEL_HOME/app/data $XYROPANEL_HOME/app/.next $XYROPANEL_HOME/backups
LockPersonality=true
SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

[Install]
WantedBy=multi-user.target
UNIT
  systemctl daemon-reload
  systemctl enable xyropanel.service
  systemctl restart xyropanel.service
}

write_maintenance_timer() {
  cat > /etc/systemd/system/xyropanel-daily-maintenance.service <<UNIT
[Unit]
Description=xyroPanel daily maintenance checks
After=xyropanel.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
EnvironmentFile=$XYROPANEL_HOME/app/.env
ExecStart=/usr/bin/curl -fsS -X POST -H "Authorization: Bearer \${XYROPANEL_MAINTENANCE_TOKEN}" http://127.0.0.1:$XYROPANEL_PORT/api/maintenance/daily
UNIT

  cat > /etc/systemd/system/xyropanel-daily-maintenance.timer <<'UNIT'
[Unit]
Description=Run xyroPanel daily maintenance checks

[Timer]
OnCalendar=*-*-* 03:17:00
Persistent=true
RandomizedDelaySec=30m

[Install]
WantedBy=timers.target
UNIT

  systemctl daemon-reload
  systemctl enable --now xyropanel-daily-maintenance.timer
}

write_nginx() {
  local server_name="_"
  if [ -n "$XYROPANEL_DOMAIN" ]; then
    server_name="$XYROPANEL_DOMAIN"
  fi

  install -d -m 0755 "$XYROPANEL_ACME_DIR" "$XYROPANEL_TLS_DIR"

  local ssl_certificate="$XYROPANEL_TLS_DIR/self-signed.crt"
  local ssl_certificate_key="$XYROPANEL_TLS_DIR/self-signed.key"
  if [ -n "$XYROPANEL_DOMAIN" ] && [ -f "/etc/letsencrypt/live/$XYROPANEL_DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$XYROPANEL_DOMAIN/privkey.pem" ]; then
    ssl_certificate="/etc/letsencrypt/live/$XYROPANEL_DOMAIN/fullchain.pem"
    ssl_certificate_key="/etc/letsencrypt/live/$XYROPANEL_DOMAIN/privkey.pem"
  elif [ ! -f "$ssl_certificate" ] || [ ! -f "$ssl_certificate_key" ]; then
    openssl req -x509 -nodes -newkey rsa:2048 -days 30 \
      -subj "/CN=${XYROPANEL_DOMAIN:-xyropanel.local}" \
      -keyout "$ssl_certificate_key" \
      -out "$ssl_certificate"
    chmod 0600 "$ssl_certificate_key"
    chmod 0644 "$ssl_certificate"
  fi

  cat > /etc/nginx/conf.d/xyropanel.conf <<NGINX
server {
  listen 80;
  server_name $server_name;

  client_max_body_size 256m;

  location /.well-known/acme-challenge/ {
    root $XYROPANEL_ACME_DIR;
  }

  location / {
    return 301 https://\$host:$XYROPANEL_HTTPS_PORT\$request_uri;
  }
}

server {
  listen $XYROPANEL_HTTP_PORT;
  server_name $server_name;

  location / {
    return 301 https://\$host:$XYROPANEL_HTTPS_PORT\$request_uri;
  }
}

server {
  listen $XYROPANEL_HTTPS_PORT ssl http2;
  server_name $server_name;

  ssl_certificate $ssl_certificate;
  ssl_certificate_key $ssl_certificate_key;
  ssl_session_cache shared:xyroPanelSSL:10m;
  ssl_session_timeout 1d;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers off;

  client_max_body_size 256m;

  location / {
    proxy_pass http://127.0.0.1:$XYROPANEL_PORT;
    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";
  }
}
NGINX
  nginx -t
  systemctl reload nginx
}

configure_tls() {
  if [ -z "$XYROPANEL_DOMAIN" ]; then
    return
  fi
  if ! getent ahosts "$XYROPANEL_DOMAIN" >/dev/null 2>&1; then
    log "Skipping Let's Encrypt for $XYROPANEL_DOMAIN because DNS does not resolve yet. Add the A/AAAA record, then rerun the installer."
    return
  fi
  if command -v certbot >/dev/null 2>&1; then
    certbot certonly --webroot -w "$XYROPANEL_ACME_DIR" -d "$XYROPANEL_DOMAIN" --non-interactive --agree-tos -m "$XYROPANEL_EMAIL" || true
    write_nginx
  fi
}

configure_firewall() {
  if command -v ufw >/dev/null 2>&1; then
    ufw allow OpenSSH >/dev/null 2>&1 || true
    ufw allow 80/tcp >/dev/null 2>&1 || true
    ufw allow "$XYROPANEL_HTTP_PORT/tcp" >/dev/null 2>&1 || true
    ufw allow "$XYROPANEL_HTTPS_PORT/tcp" >/dev/null 2>&1 || true
  elif command -v firewall-cmd >/dev/null 2>&1; then
    firewall-cmd --permanent --add-service=http >/dev/null 2>&1 || true
    firewall-cmd --permanent --add-port="$XYROPANEL_HTTP_PORT/tcp" >/dev/null 2>&1 || true
    firewall-cmd --permanent --add-port="$XYROPANEL_HTTPS_PORT/tcp" >/dev/null 2>&1 || true
    firewall-cmd --reload >/dev/null 2>&1 || true
  fi
}

wait_for_panel() {
  log "Running local panel health check"
  local url="http://127.0.0.1:$XYROPANEL_PORT/api/setup/status"
  local attempt
  for attempt in $(seq 1 30); do
    if curl -fsS "$url" >/dev/null 2>&1; then
      log "xyroPanel panel responded successfully"
      return
    fi
    sleep 2
  done
  systemctl status xyropanel.service --no-pager || true
  journalctl -u xyropanel.service -n 80 --no-pager || true
  fail "xyroPanel did not respond on $url"
}

install_local_agent() {
  if [ "$XYROPANEL_INSTALL_AGENT" != "1" ]; then
    return
  fi

  log "Installing local xyroPanel server agent"

  local manifest="$XYROPANEL_APP_DIR/public/releases/xyropanel-agent/latest.json"
  if [ ! -f "$manifest" ]; then
    warn "Local agent release manifest was not found at $manifest. Server health will stay pending until an agent is enrolled."
    return
  fi

  local arch
  arch="$(uname -m)"
  case "$arch" in
    x86_64|amd64) arch="amd64" ;;
    aarch64|arm64) arch="arm64" ;;
    *) warn "Unsupported local agent architecture: $arch"; return ;;
  esac

  local artifact_info
  artifact_info="$(python3 - "$manifest" "$arch" <<'PY'
import json
import sys
from pathlib import Path

manifest = json.loads(Path(sys.argv[1]).read_text())
arch = sys.argv[2]
for artifact in manifest.get("artifacts", []):
    if artifact.get("os") == "linux" and artifact.get("arch") == arch:
        print(manifest.get("version", "0.1.0"))
        print(artifact["url"].lstrip("/"))
        print(artifact["sha256"])
        break
else:
    raise SystemExit(f"no linux/{arch} artifact")
PY
)"

  local agent_version artifact_rel artifact_sha artifact_path actual_sha
  agent_version="$(printf '%s\n' "$artifact_info" | sed -n '1p')"
  artifact_rel="$(printf '%s\n' "$artifact_info" | sed -n '2p')"
  artifact_sha="$(printf '%s\n' "$artifact_info" | sed -n '3p')"
  artifact_path="$XYROPANEL_APP_DIR/public/$artifact_rel"
  if [ ! -f "$artifact_path" ]; then
    warn "Local agent binary was not found at $artifact_path."
    return
  fi

  if command -v sha256sum >/dev/null 2>&1; then
    actual_sha="$(sha256sum "$artifact_path" | awk '{print $1}')"
  else
    actual_sha="$(shasum -a 256 "$artifact_path" | awk '{print $1}')"
  fi
  if [ "$actual_sha" != "$artifact_sha" ]; then
    warn "Local agent checksum mismatch. Server health will stay pending until an agent is enrolled manually."
    return
  fi

  install -m 0755 "$artifact_path" /usr/local/bin/xyropanel-agent

  local db_path="$XYROPANEL_APP_DIR/data/xyropanel.db"
  local hostname_value server_id agent_token token_hash now
  hostname_value="$(hostname -f 2>/dev/null || hostname)"
  server_id="$(sqlite3 "$db_path" "SELECT id FROM managed_servers WHERE hostname = '$hostname_value' LIMIT 1;" 2>/dev/null || true)"
  if [ -z "$server_id" ]; then
    server_id="srv_local_$(date +%s)_$(openssl rand -hex 3)"
  fi
  agent_token="xyr_agent_$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=')"
  token_hash="$(printf '%s' "$agent_token" | openssl dgst -sha256 -r | awk '{print $1}')"
  now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"

  sqlite3 "$db_path" <<SQL
INSERT INTO managed_servers (
  id, name, hostname, distro, agent_version, status, cpu_percent, ram_percent,
  storage_percent, load_average, mail_queue, pending_updates, reboot_required,
  last_heartbeat_at, created_at
) VALUES (
  '$server_id', 'Local Server', '$hostname_value', '$DISTRO_ID', '$agent_version',
  'healthy', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '$now'
)
ON CONFLICT(hostname) DO UPDATE SET
  agent_version = excluded.agent_version,
  status = 'healthy';

DELETE FROM server_capabilities WHERE server_id = '$server_id' AND name IN ('system_updates_check', 'system_updates_apply');
INSERT INTO server_capabilities VALUES ('cap_' || '$server_id' || '_system_updates_check', '$server_id', 'system_updates_check', 0, 0, 'ubuntu,debian,almalinux,rocky', 300);
INSERT INTO server_capabilities VALUES ('cap_' || '$server_id' || '_system_updates_apply', '$server_id', 'system_updates_apply', 1, 0, 'ubuntu,debian,almalinux,rocky', 3600);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_inspect_logs', '$server_id', 'inspect_logs', 0, 0, 'ubuntu,debian,almalinux,rocky', 20);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_list_services', '$server_id', 'list_services', 0, 0, 'ubuntu,debian,almalinux,rocky', 15);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_create_site', '$server_id', 'create_site', 1, 1, 'ubuntu,debian,almalinux,rocky', 120);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_add_domain', '$server_id', 'add_domain', 1, 1, 'ubuntu,debian,almalinux,rocky', 90);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_dns_record', '$server_id', 'dns_record', 1, 1, 'ubuntu,debian,almalinux,rocky', 45);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_create_database', '$server_id', 'create_database', 1, 1, 'ubuntu,debian,almalinux,rocky', 120);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_issue_ssl', '$server_id', 'issue_ssl', 1, 1, 'ubuntu,debian,almalinux,rocky', 180);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_run_backup', '$server_id', 'run_backup', 1, 0, 'ubuntu,debian,almalinux,rocky', 3600);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_restore_backup', '$server_id', 'restore_backup', 1, 1, 'ubuntu,debian,almalinux,rocky', 3600);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_restore_readiness', '$server_id', 'restore_readiness', 0, 0, 'ubuntu,debian,almalinux,rocky', 180);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_site_status', '$server_id', 'site_status', 1, 1, 'ubuntu,debian,almalinux,rocky', 90);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_site_runtime', '$server_id', 'site_runtime', 1, 1, 'ubuntu,debian,almalinux,rocky', 90);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_file_write', '$server_id', 'file_write', 1, 1, 'ubuntu,debian,almalinux,rocky', 60);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_mailbox_create', '$server_id', 'mailbox_create', 1, 1, 'ubuntu,debian,almalinux,rocky', 90);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_mailbox_update', '$server_id', 'mailbox_update', 1, 1, 'ubuntu,debian,almalinux,rocky', 90);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_mailbox_delete', '$server_id', 'mailbox_delete', 1, 1, 'ubuntu,debian,almalinux,rocky', 90);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_mail_alias_update', '$server_id', 'mail_alias_update', 1, 1, 'ubuntu,debian,almalinux,rocky', 60);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_mail_alias_delete', '$server_id', 'mail_alias_delete', 1, 1, 'ubuntu,debian,almalinux,rocky', 60);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_mail_spam_check', '$server_id', 'mail_spam_check', 0, 0, 'ubuntu,debian,almalinux,rocky', 120);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_cpanel_import', '$server_id', 'cpanel_import', 1, 1, 'ubuntu,debian,almalinux,rocky', 7200);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_full_server_backup', '$server_id', 'full_server_backup', 1, 0, 'ubuntu,debian,almalinux,rocky', 7200);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_security_tool_status', '$server_id', 'security_tool_status', 0, 0, 'ubuntu,debian,almalinux,rocky', 60);
INSERT OR IGNORE INTO server_capabilities VALUES ('cap_' || '$server_id' || '_security_tool_install', '$server_id', 'security_tool_install', 1, 1, 'ubuntu,debian,almalinux,rocky', 600);

INSERT INTO agent_credentials VALUES ('agt_local_' || strftime('%s','now') || '_' || lower(hex(randomblob(3))), '$server_id', '$token_hash', '$now', NULL, NULL);
SQL

  chown "$XYROPANEL_USER:$XYROPANEL_USER" "$db_path"

  install -d -m 0750 /etc/xyropanel /var/lib/xyropanel
  umask 077
  cat > /etc/xyropanel/agent.env <<ENV
XYROPANEL_URL=http://127.0.0.1:$XYROPANEL_PORT
XYROPANEL_AGENT_TOKEN=$agent_token
ENV

  cat > /etc/systemd/system/xyropanel-agent.service <<'UNIT'
[Unit]
Description=xyroPanel local server agent
After=network-online.target xyropanel.service
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=/etc/xyropanel/agent.env
ExecStart=/usr/local/bin/xyropanel-agent
Restart=always
RestartSec=10
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
ReadWritePaths=/var/lib/xyropanel /etc/xyropanel /var/www/xyropanel /var/backups/xyropanel /var/vmail /etc/nginx/conf.d /etc/postfix /etc/dovecot/conf.d /run /tmp
LockPersonality=true
SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

[Install]
WantedBy=multi-user.target
UNIT

  systemctl daemon-reload
  systemctl enable --now xyropanel-agent.service
}

check_public_ports() {
  command -v ss >/dev/null 2>&1 || return
  local port
  for port in "$XYROPANEL_HTTP_PORT" "$XYROPANEL_HTTPS_PORT"; do
    if ss -ltn | awk 'NR > 1 {print $4}' | grep -Eq "[:.]$port$"; then
      if [ -f /etc/nginx/conf.d/xyropanel.conf ] && grep -q "listen $port" /etc/nginx/conf.d/xyropanel.conf; then
        continue
      fi
      fail "Port $port is already in use. Set XYROPANEL_HTTP_PORT or XYROPANEL_HTTPS_PORT to another lucky 4-digit port and rerun."
    fi
  done
}

cat <<MSG
xyroPanel SSH installer
Distro: $DISTRO_ID
Install user: $XYROPANEL_USER
Install path: $XYROPANEL_APP_DIR
Panel port: $XYROPANEL_PORT
Public HTTP redirect port: $XYROPANEL_HTTP_PORT
Public HTTPS panel port: $XYROPANEL_HTTPS_PORT
Domain: ${XYROPANEL_DOMAIN:-not set, using server IP with self-signed HTTPS}
Auto-detected domain: ${XYROPANEL_DOMAIN_AUTO_DETECTED}
Upgrade backups: $XYROPANEL_BACKUP_DIR

Provider credentials, billing modules, domain modules, paid upgrades, backup
storage credentials, Google/Zoho email, and every user's 2FA are configured
after login inside xyroPanel. They are not collected during SSH installation.
MSG

log "Installing OS packages"
case "$DISTRO_ID" in
  ubuntu|debian) install_base_packages_debian ;;
  almalinux|rocky) install_base_packages_rpm ;;
esac

if [ "$XYROPANEL_INSTALL_STACK" = "1" ]; then
  log "Installing hosting stack packages"
  case "$DISTRO_ID" in
    ubuntu|debian) install_hosting_stack_debian ;;
    almalinux|rocky) install_hosting_stack_rpm ;;
  esac
  enable_stack_services
fi

log "Creating xyroPanel service account"
create_user

backup_existing_install

log "Installing xyroPanel source"
install_source

log "Configuring and building xyroPanel"
configure_app

log "Writing systemd service"
write_service
write_maintenance_timer

wait_for_panel

install_local_agent

log "Configuring nginx"
check_public_ports
write_nginx
configure_tls
configure_firewall

cat <<MSG

xyroPanel installation complete.

Panel URL:
  ${XYROPANEL_DOMAIN:+https://$XYROPANEL_DOMAIN:$XYROPANEL_HTTPS_PORT}
  ${XYROPANEL_DOMAIN:-https://$(hostname -I 2>/dev/null | awk '{print $1}'):$XYROPANEL_HTTPS_PORT/}

Service:
  systemctl status xyropanel --no-pager

Logs:
  journalctl -u xyropanel -f
  tail -f $XYROPANEL_INSTALL_LOG

Rollback:
  ${LAST_BACKUP_ARCHIVE:+$XYROPANEL_HOME/rollback-last.sh}
  ${LAST_BACKUP_ARCHIVE:-No pre-upgrade backup was needed for this install.}

Next:
  Open the panel in your browser and complete first-run setup. Optional modules
  like Stripe, PayPal, Openprovider, Enom, Google/Zoho mail, agency upgrades,
  billing upgrades, and backup storage are configured after login.
MSG
