#!/usr/bin/env bash
# vgwm — VersityGW Manager (AlmaLinux 8/9)
#
#   vgwm install [--template FILE] [--rpm FILE]  полная установка
#   vgwm firewall                                только firewall на nftables (без VersityGW): сброс всех
#                                                правил и создание заново в интерактиве
#   vgwm update                                  обновление VersityGW из GitHub Releases
#   vgwm self-install                            только установить vgwm в /usr/local/bin
#
# --template FILE  — файл-образец конфига: берутся только активные (не закомментированные)
#                    строки, серверо-зависимые параметры запрашиваются интерактивно.
# --rpm FILE       — локальный RPM вместо загрузки последнего релиза.
# --debug          — подробный вывод (например, загруженные правила nftables).

set -Eeuo pipefail

INSTANCE="mnogoweb"
CONF="/etc/versitygw.d/${INSTANCE}.conf"
SERVICE="versitygw@${INSTANCE}"
STATE_DIR="/etc/vgwm"
CLIENTS_FILE="${STATE_DIR}/clients.list"
GH_API="https://api.github.com/repos/versity/versitygw/releases/latest"

ADMIN_IPS=(94.250.251.190 46.161.40.8)          # SSH + 80/443
MON_IPS=(212.237.218.213 37.228.116.152)        # Zabbix + Beszel
SSH_PORT=9999
ZABBIX_PORT=10050
BESZEL_PORT=45876

# ---------- helpers ----------
log()  { echo -e "\e[32m[+]\e[0m $*"; }
warn() { echo -e "\e[33m[!]\e[0m $*" >&2; }
die()  { echo -e "\e[31m[x]\e[0m $*" >&2; exit 1; }

ask() {  # ask "Вопрос" "default" -> stdout
    local prompt="$1" def="${2:-}" ans
    if [[ -n "$def" ]]; then read -rp "$prompt [$def]: " ans; echo "${ans:-$def}"
    else read -rp "$prompt: " ans; echo "$ans"; fi
}
ask_secret() {
    local prompt="$1" def="${2:-}" ans
    read -rsp "$prompt${def:+ [оставить текущий]}: " ans; echo >&2
    echo "${ans:-$def}"
}
yes_no() { local a; read -rp "$1 [y/N]: " a; [[ "$a" =~ ^[YyДд]$ ]]; }

valid_ip() {  # IPv4/IPv6 с опциональным /маска
    python3 - "$1" <<'PY' 2>/dev/null
import ipaddress, sys
ipaddress.ip_network(sys.argv[1], strict=False)
PY
}
ip_family() { [[ "$1" == *:* ]] && echo ipv6 || echo ipv4; }

need_root() { [[ $EUID -eq 0 ]] || die "Запускать от root"; }

# ---------- RPM ----------
rpm_arch_regex() {
    case "$(uname -m)" in
        x86_64)  echo 'x86_64|amd64' ;;
        aarch64) echo 'aarch64|arm64' ;;
        *) die "Неподдерживаемая архитектура: $(uname -m)" ;;
    esac
}

download_latest_rpm() {  # -> путь к rpm в stdout
    local json url tag tmp
    command -v curl >/dev/null || dnf -y install curl >&2
    json="$(curl -fsSL "$GH_API")" || die "Не удалось получить данные релиза с GitHub"
    tag="$(grep -oP '"tag_name":\s*"\K[^"]+' <<<"$json" | head -1)"
    url="$(grep -oP '"browser_download_url":\s*"\K[^"]+\.rpm' <<<"$json" \
           | grep -Ei "$(rpm_arch_regex)" | head -1)"
    [[ -n "$url" ]] || die "В релизе $tag не найден RPM для $(uname -m)"
    tmp="$(mktemp -d)"
    log "Скачиваю $tag: $url" >&2
    curl -fL --retry 3 -o "$tmp/$(basename "$url")" "$url" >&2
    echo "$tmp/$(basename "$url")"
}

install_rpm() {
    local rpm="$1"
    [[ -f "$rpm" ]] || die "RPM не найден: $rpm"
    dnf -y install "$rpm"
}

# ---------- config ----------
set_kv() {  # set_kv KEY VALUE FILE — заменить активный ключ или дописать
    local k="$1" v="$2" f="$3"
    if grep -qE "^[[:space:]]*${k}=" "$f"; then
        sed -i -E "s|^[[:space:]]*${k}=.*|${k}=${v//|/\\|}|" "$f"
    else
        echo "${k}=${v}" >>"$f"
    fi
}
get_kv() { grep -E "^[[:space:]]*$1=" "$2" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d '"' || true; }

# Активные параметры эталонного шаблона mnogoweb.
# __REGION__, ключи и пути подставляются при установке.
default_template() {
cat <<'EOF'
VGW_BACKEND=posix
VGW_BACKEND_ARG=/data/s3
ROOT_ACCESS_KEY_ID=
ROOT_SECRET_ACCESS_KEY=
VGW_PORT=:443
VGW_REGION=__REGION__
VGW_CERT=/etc/versitygw.d/ssl.cert
VGW_KEY=/etc/versitygw.d/ssl.key
VGW_VIRTUAL_DOMAIN=s3.bs-__REGION__.mnogoweb.com
VGW_DISABLE_STRICT_BUCKET_NAMES=true
VGW_MP_MAX_PARTS=100000
VGW_COPY_OBJECT_THRESHOLD=64424509440
VGW_WEBUI_S3_PREFIX=/ui
VGW_WEBUI_CERT=/etc/versitygw.d/ssl.cert
VGW_WEBUI_KEY=/etc/versitygw.d/ssl.key
VGW_CORS_ALLOW_ORIGIN=https://bs-__REGION__.mnogoweb.com
VGW_WEBUI_GATEWAYS=https://s3.bs-__REGION__.mnogoweb.com
VGW_DEBUG=true
EOF
}

gen_key() { python3 -c "import secrets,string,sys;a=string.ascii_letters+string.digits;print(''.join(secrets.choice(a) for _ in range(int(sys.argv[1]))))" "$1"; }

place_tls() {  # place_tls DEST "описание" — копирует файл в DEST, если его ещё нет
    local dest="$1" what="$2" src
    if [[ -f "$dest" ]]; then
        src="$(ask "$what уже есть ($dest). Путь к новому файлу (пусто — оставить)" "")"
    else
        src="$(ask "Путь к файлу: $what (будет скопирован в $dest)" "")"
    fi
    if [[ -n "$src" ]]; then
        [[ -f "$src" ]] || die "Файл не найден: $src"
        install -m 0640 -o root -g versitygw "$src" "$dest"
    fi
    [[ -f "$dest" ]] || die "$what отсутствует: $dest — без него сервис не запустится"
}

configure_vgw() {
    local template="${1:-}" tmp
    tmp="$(mktemp)"
    if [[ -n "$template" ]]; then
        [[ -f "$template" ]] || die "Шаблон не найден: $template"
        # только активные строки: без комментариев и пустых
        grep -Ev '^[[:space:]]*(#|$)' "$template" | sed -E 's/[[:space:]]+$//' >"$tmp"
        log "Из шаблона взято активных параметров: $(wc -l <"$tmp")"
    else
        default_template >"$tmp"
    fi

    # значения уже существующего конфига — как дефолты при повторном запуске
    local old="$CONF"; [[ -f "$old" ]] || old=/dev/null

    echo "--- Серверо-зависимые параметры ---"
    local region domain backend_arg access secret def_reg cert key iam
    def_reg="$(get_kv VGW_REGION "$old")"; def_reg="${def_reg:-$(get_kv VGW_REGION "$tmp")}"
    [[ "$def_reg" == "__REGION__" ]] && def_reg=""
    region="$(ask "Регион (VGW_REGION), напр. us1" "$def_reg")"
    [[ "$region" =~ ^[a-z0-9-]+$ ]] || die "Некорректный регион: $region"
    domain="$(ask "Базовый домен" "bs-${region}.mnogoweb.com")"

    backend_arg="$(get_kv VGW_BACKEND_ARG "$old")"; backend_arg="${backend_arg:-$(get_kv VGW_BACKEND_ARG "$tmp")}"
    backend_arg="$(ask "Каталог данных (VGW_BACKEND_ARG)" "$backend_arg")"

    access="$(get_kv ROOT_ACCESS_KEY_ID "$old")"
    secret="$(get_kv ROOT_SECRET_ACCESS_KEY "$old")"
    if [[ -z "$access" || -z "$secret" ]] || yes_no "Сгенерировать новые root-ключи?"; then
        if yes_no "Ввести root-ключи вручную (иначе — сгенерировать)?"; then
            access="$(ask "Root access key" "")"
            secret="$(ask_secret "Root secret key" "")"
        else
            access="$(gen_key 16)"; secret="$(gen_key 32)"
        fi
    fi
    [[ -n "$access" && -n "$secret" ]] || die "Access/secret обязательны"

    sed -i "s/__REGION__/${region}/g" "$tmp"
    set_kv VGW_REGION "$region" "$tmp"
    set_kv VGW_VIRTUAL_DOMAIN "s3.${domain}" "$tmp"
    set_kv VGW_CORS_ALLOW_ORIGIN "https://${domain}" "$tmp"
    set_kv VGW_WEBUI_GATEWAYS "https://s3.${domain}" "$tmp"
    set_kv VGW_BACKEND_ARG "$backend_arg" "$tmp"
    set_kv ROOT_ACCESS_KEY_ID "$access" "$tmp"
    set_kv ROOT_SECRET_ACCESS_KEY "$secret" "$tmp"

    mkdir -p /etc/versitygw.d
    [[ -f "$CONF" ]] && cp -a "$CONF" "${CONF}.bak.$(date +%F_%H%M%S)"
    install -m 0640 -o root -g versitygw "$tmp" "$CONF"
    rm -f "$tmp"

    # TLS: сертификат для s3.${domain} и *.s3.${domain} (virtual-host бакеты)
    cert="$(get_kv VGW_CERT "$CONF")"; key="$(get_kv VGW_KEY "$CONF")"
    [[ -n "$cert" ]] && place_tls "$cert" "TLS-сертификат (fullchain)"
    [[ -n "$key"  ]] && place_tls "$key"  "TLS-ключ"

    # каталоги и права
    mkdir -p "$backend_arg"; chown versitygw:versitygw "$backend_arg"
    iam="$(get_kv VGW_IAM_DIR "$CONF")"
    [[ -n "$iam" ]] && { mkdir -p "$iam"; chown versitygw:versitygw "$iam"; }

    log "Конфиг записан: $CONF"
    echo "    Регион:     $region"
    echo "    S3:         https://s3.${domain}  (WebUI: https://s3.${domain}/ui)"
    echo "    Access key: $access"
    echo "    Secret key: $secret   <- сохраните"
}

enable_service() {
    # разрешаем непривилегированному пользователю слушать 80/443
    mkdir -p "/etc/systemd/system/versitygw@.service.d"
    cat >/etc/systemd/system/versitygw@.service.d/override.conf <<'EOF'
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
EOF
    systemctl daemon-reload
    systemctl enable "$SERVICE"
    systemctl restart "$SERVICE"
    sleep 2
    systemctl is-active --quiet "$SERVICE" \
        && log "Сервис $SERVICE запущен" \
        || { journalctl -u "$SERVICE" -n 30 --no-pager; die "Сервис не стартовал"; }
}

# ---------- firewall (nftables) ----------
read_ip_list() {  # интерактивный ввод списка IP -> CLIENTS[]
    local def="" input
    [[ -f "$CLIENTS_FILE" ]] && def="$(tr '\n' ' ' <"$CLIENTS_FILE" | sed 's/ $//')"
    echo "Список IP/подсетей клиентов для 80/443 и FTP (через пробел или запятую)."
    input="$(ask "IP клиентов" "$def")"
    CLIENTS=()
    for ip in ${input//,/ }; do
        valid_ip "$ip" && CLIENTS+=("$ip") || warn "Пропущен некорректный адрес: $ip"
    done
    mkdir -p "$STATE_DIR"
    printf '%s\n' "${CLIENTS[@]}" >"$CLIENTS_FILE"
}

NFT_RULES="/etc/nftables/vgwm.nft"
NFT_MAIN="/etc/sysconfig/nftables.conf"
ROLLBACK_SEC=60

# элементы set'а для одного семейства: nft_elems ipv4|ipv6 IP...
nft_elems() {
    local fam="$1"; shift
    local out=() ip
    for ip in "$@"; do [[ "$(ip_family "$ip")" == "$fam" ]] && out+=("$ip"); done
    (( ${#out[@]} )) && { local IFS=,; echo "elements = { ${out[*]} };"; }
}

gen_ruleset() {  # gen_ruleset PASV -> stdout
    local pasv="$1"
    cat <<EOF
#!/usr/sbin/nft -f
# Сгенерировано vgwm $(date '+%F %T'). Ручные правки будут перезаписаны.
flush ruleset

table inet vgwm {
    set admin4   { type ipv4_addr; flags interval; $(nft_elems ipv4 "${ADMIN_IPS[@]}") }
    set admin6   { type ipv6_addr; flags interval; $(nft_elems ipv6 "${ADMIN_IPS[@]}") }
    set clients4 { type ipv4_addr; flags interval; $(nft_elems ipv4 "${CLIENTS[@]}") }
    set clients6 { type ipv6_addr; flags interval; $(nft_elems ipv6 "${CLIENTS[@]}") }
    set mon4     { type ipv4_addr; flags interval; $(nft_elems ipv4 "${MON_IPS[@]}") }
    set mon6     { type ipv6_addr; flags interval; $(nft_elems ipv6 "${MON_IPS[@]}") }

    chain input {
        type filter hook input priority filter; policy drop;

        iif lo accept
        ct state established,related accept
        ct state invalid drop

        # IPv6 не работает без Neighbor Discovery
        icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, nd-router-solicit } ip6 hoplimit 255 accept

        # SSH
        tcp dport $SSH_PORT ip  saddr @admin4 accept
        tcp dport $SSH_PORT ip6 saddr @admin6 accept

        # HTTP/HTTPS: админы + клиенты
        tcp dport { 80, 443 } ip  saddr @admin4   accept
        tcp dport { 80, 443 } ip6 saddr @admin6   accept
        tcp dport { 80, 443 } ip  saddr @clients4 accept
        tcp dport { 80, 443 } ip6 saddr @clients6 accept

        # FTP (control + passive)
        tcp dport { 21, $pasv } ip  saddr @clients4 accept
        tcp dport { 21, $pasv } ip6 saddr @clients6 accept

        # Zabbix-agent, Beszel-agent
        tcp dport { $ZABBIX_PORT, $BESZEL_PORT } ip  saddr @mon4 accept
        tcp dport { $ZABBIX_PORT, $BESZEL_PORT } ip6 saddr @mon6 accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}
EOF
}

ensure_nftables() {
    rpm -q nftables >/dev/null 2>&1 || dnf -y install nftables || die "Не удалось установить nftables"

    # всё, что тоже управляет netfilter, должно быть выключено
    local svc
    for svc in firewalld iptables ip6tables ufw; do
        if systemctl is-active --quiet "$svc" 2>/dev/null || systemctl is-enabled --quiet "$svc" 2>/dev/null; then
            warn "Найден конкурирующий сервис: $svc"
            yes_no "Остановить, отключить и замаскировать $svc?" || die "Конфликт с $svc — прервано"
            systemctl disable --now "$svc" >/dev/null 2>&1 || true
            systemctl mask "$svc" >/dev/null 2>&1 || true
        fi
    done

    [[ "$(systemctl is-enabled nftables 2>/dev/null)" == "masked" ]] && systemctl unmask nftables
    command -v nft >/dev/null || die "nft не найден"
    log "nftables $(nft --version | awk '{print $2}')"
}

apply_with_rollback() {  # apply_with_rollback FILE
    local new="$1" backup
    backup="$(mktemp)"
    nft list ruleset >"$backup"

    nft -c -f "$new" || { rm -f "$backup"; die "Ошибка синтаксиса правил: $new"; }
    nft -f "$new"    || { rm -f "$backup"; die "Не удалось применить правила"; }

    if [[ -t 0 && -n "${SSH_CONNECTION:-}" ]]; then
        warn "Правила применены. Откройте НОВУЮ SSH-сессию и проверьте доступ."
        local ans=""
        read -r -t "$ROLLBACK_SEC" -p "Введите 'ok' в течение ${ROLLBACK_SEC} c, иначе откат: " ans || true
        echo
        if [[ "$ans" != "ok" ]]; then
            { echo "flush ruleset"; cat "$backup"; } | nft -f -
            rm -f "$backup"
            die "Подтверждения нет — правила откатаны, изменения не сохранены"
        fi
    fi
    rm -f "$backup"
}

persist_ruleset() {
    install -d -m 0755 "$(dirname "$NFT_RULES")"
    install -m 0600 "$1" "$NFT_RULES"
    [[ -f "$NFT_MAIN" ]] && ! grep -qx "include \"$NFT_RULES\"" "$NFT_MAIN" \
        && cp -a "$NFT_MAIN" "${NFT_MAIN}.bak.$(date +%F_%H%M%S)"
    cat >"$NFT_MAIN" <<EOF
# Управляется vgwm. Все правила — в $NFT_RULES
include "$NFT_RULES"
EOF
    systemctl enable nftables >/dev/null 2>&1
    systemctl restart nftables || { journalctl -u nftables -n 20 --no-pager; die "nftables.service не стартовал"; }
}

configure_firewall() {
    ensure_nftables

    read_ip_list
    local pasv; pasv="$(ask "Диапазон пассивных портов FTP" "$(cat "$STATE_DIR/ftp_pasv" 2>/dev/null || echo 30000-31000)")"
    [[ "$pasv" =~ ^[0-9]+-[0-9]+$ ]] || die "Неверный диапазон: $pasv"
    echo "$pasv" >"$STATE_DIR/ftp_pasv"

    # защита от потери доступа
    local me="${SSH_CLIENT%% *}"
    if [[ -n "$me" ]] && ! printf '%s\n' "${ADMIN_IPS[@]}" | grep -qx "$me"; then
        warn "Ваш текущий IP $me НЕ входит в список SSH ($SSH_PORT): ${ADMIN_IPS[*]}"
        yes_no "Продолжить?" || die "Отменено"
    fi
    ss -ltnH "sport = :$SSH_PORT" 2>/dev/null | grep -q . \
        || warn "Никто не слушает порт $SSH_PORT — после применения SSH будет недоступен"

    local tmp; tmp="$(mktemp)"
    gen_ruleset "$pasv" >"$tmp"

    log "Сброс всех правил и применение нового набора"
    apply_with_rollback "$tmp"
    persist_ruleset "$tmp"
    rm -f "$tmp"

    log "Firewall применён и сохранён в $NFT_RULES"
    if (( DEBUG )); then
        echo "--- [debug] загруженные правила ---"
        nft list table inet vgwm
    fi
}

# ---------- update ----------
update_vgw() {
    local cur rpm new
    cur="$(rpm -q versitygw 2>/dev/null || echo 'не установлен')"
    rpm="$(download_latest_rpm)"
    new="$(rpm -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' "$rpm")"
    log "Установлено: $cur | Доступно: $new"
    if [[ "$cur" == "$new" ]]; then log "Уже актуальная версия"; rm -rf "$(dirname "$rpm")"; return; fi
    dnf -y install "$rpm"
    rm -rf "$(dirname "$rpm")"
    systemctl daemon-reload
    systemctl restart "$SERVICE"
    systemctl is-active --quiet "$SERVICE" && log "Обновлено, сервис работает" \
        || { journalctl -u "$SERVICE" -n 30 --no-pager; die "Сервис не стартовал после обновления"; }
    versitygw --version || true
}

# ---------- self-install ----------
VGWM_BIN="/usr/local/bin/vgwm"

self_install() {
    local self; self="$(readlink -f "$0" 2>/dev/null || true)"
    [[ -f "$self" && "$self" != "$VGWM_BIN" ]] || return 0   # уже установлен или запуск через pipe
    if [[ -f "$VGWM_BIN" ]] && cmp -s "$self" "$VGWM_BIN"; then return 0; fi
    local action="Установлен"; [[ -f "$VGWM_BIN" ]] && action="Обновлён"
    install -m 0755 -o root -g root "$self" "$VGWM_BIN"
    log "$action vgwm → $VGWM_BIN (дальше можно запускать просто: vgwm <режим>)"
}

usage() { sed -n '2,14p' "$VGWM_BIN" 2>/dev/null || sed -n '2,14p' "$0"; }

# ---------- main ----------
DEBUG=0

main() {
    # --debug допускается в любом месте командной строки
    local args=() a
    for a in "$@"; do [[ "$a" == "--debug" ]] && DEBUG=1 || args+=("$a"); done
    set -- "${args[@]+"${args[@]}"}"

    need_root
    self_install
    local mode="${1:-}"; shift || true
    local template="" rpm=""
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --template) template="$2"; shift 2 ;;
            --rpm)      rpm="$2"; shift 2 ;;
            *) die "Неизвестный параметр: $1" ;;
        esac
    done

    case "$mode" in
        install)
            [[ -n "$rpm" ]] || rpm="$(download_latest_rpm)"
            install_rpm "$rpm"
            configure_vgw "$template"
            enable_service
            configure_firewall
            ;;
        firewall|firewall-only|fw) configure_firewall ;;
        update)   update_vgw ;;
        self-install) log "vgwm установлен в $VGWM_BIN" ;;
        ""|help|-h|--help) usage ;;
        *) usage; exit 1 ;;
    esac
}
main "$@"
