#!/bin/bash
################################################################################
# Script Name: version/3.7.2/UPDATE.sh
# Description: Apply version 3.7.2 - editions install different servers:
#              - the module registry decides what a server has; a Community
#                install no longer installs the services it cannot use
#              - a Community server gets a webmail it never had: this update
#                installs it on a server that has mail and no webmail yet
#              - mail filters are enforced at delivery (Sieve/ManageSieve),
#                so a rule applies whether or not anybody is looking
#              - the panels' Rust twins are refreshed and checked before the
#                panels restart, and the binaries are put back on failure
# Company/Brand: HiTechCloud by Pho Tue SoftWare Solutions JSC
################################################################################

set -uo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
ok()   { echo -e "[3.7.2] ${GREEN}✓${NC} $*"; }
warn() { echo -e "[3.7.2] ${YELLOW}!${NC} $*" >&2; }
bad()  { echo -e "[3.7.2] ${RED}✗${NC} $*" >&2; }
[[ $EUID -eq 0 ]] || { echo "[3.7.2] This update must run as root." >&2; exit 1; }

VERSION="3.7.2"
SRC="${HITECHCLOUD_SRC:-/opt/hitechcloud}"
CLI_DIR="/usr/local/hitechcloudcli"
BIN=/usr/local/bin
ROLLBACK="/var/lib/hitechcloud/rollback/${VERSION}"
MIRROR="${HITECHCLOUD_MIRROR:-https://mirrors.hitechcloud.org}"
DRY_RUN="${HITECHCLOUD_UPDATE_DRY_RUN:-0}"
run() { if [[ "$DRY_RUN" == 1 ]]; then echo "[3.7.2] would: $*"; else "$@"; fi; }

arch=$(uname -m); case "$arch" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; esac

mkdir -p /usr/local/hitechcloud /etc/hitechcloud "$ROLLBACK" 2>/dev/null || true
echo "$VERSION" > /usr/local/hitechcloud/version 2>/dev/null || true
echo "$VERSION" > /etc/hitechcloud/version 2>/dev/null || true

# 0. What this update may have to put back: the binaries as they are now
#    (the regular updater has already installed this release's panels;
#    update.sh keeps the previous ones as <binary>.prev since 3.7.1).
for b in hitechcloud hitechadmin hitechcloud-rustcli hitechcloud-rustpanel hitechcloud-rustadmin; do
    [[ -x "${BIN}/${b}" ]] && cp -a "${BIN}/${b}" "${ROLLBACK}/${b}" 2>/dev/null
done

# 1. The CLI tree. This release deletes the scripts hitechcloud-rustcli
#    replaced, so the copy is synced with --delete: a deleted script left
#    behind would shadow the Rust command for the dispatcher. Only the
#    tracked tree is mirrored; nothing outside the checkout is touched.
if [[ -d "${SRC}/hitechcloudcli" && -f "${SRC}/hitechcloudcli/hitechcloudcli" ]]; then
    run rsync -a --delete --exclude '.git' --exclude 'rustcli/target' "${SRC}/hitechcloudcli/" "${CLI_DIR}/" \
        && ok "CLI tree synced from ${SRC}/hitechcloudcli (retired scripts removed)" \
        || warn "the CLI tree could not be synced; retired scripts may still be present under ${CLI_DIR}"
    find "$CLI_DIR" -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true
    [[ -f "${CLI_DIR}/hitechcloudcli" ]] && run install -m 755 "${CLI_DIR}/hitechcloudcli" "${BIN}/hitechcloudcli"
    [[ -f "${CLI_DIR}/native/jail.sh" ]] && run install -m 755 "${CLI_DIR}/native/jail.sh" "${BIN}/hitechcloud-jail"
else
    warn "no checkout at ${SRC}/hitechcloudcli; the CLI tree is left as the updater copied it"
fi

# fetch <binary>: from the checkout's bin/ or encoded-<arch>/, else the mirror,
# else the GitHub release. ELF + architecture + SHA256SUMS checked before use.
fetch() {
    local b="$1" tmp candidate
    for candidate in "${SRC}/bin/${b}" "${SRC}/hitechcloudpanel/encoded-${arch}/${b}"; do
        if [[ -x "$candidate" ]] && head -c 4 "$candidate" | grep -q $'\x7fELF'; then
            run install -m 755 "$candidate" "${BIN}/${b}" && { ok "${b} installed from the checkout"; return 0; }
        fi
    done
    tmp=$(mktemp "/tmp/${b}.XXXXXX")
    if curl -4fsSL --retry 3 --connect-timeout 20 "${MIRROR}/releases/${VERSION}/${b}-linux-${arch}" -o "$tmp" 2>/dev/null \
       || curl -fsSL --retry 3 --connect-timeout 20 "https://github.com/hitechcloud-vietnam/HiTechCloud-Panel/releases/download/v${VERSION}/${b}-linux-${arch}" -o "$tmp" 2>/dev/null; then
        if ! head -c 4 "$tmp" | grep -q $'\x7fELF'; then rm -f "$tmp"; bad "${b}: the download is not an executable"; return 1; fi
        local sums want got
        sums=$(mktemp)
        if curl -4fsSL --retry 2 --connect-timeout 20 "${MIRROR}/releases/${VERSION}/SHA256SUMS" -o "$sums" 2>/dev/null; then
            want=$(awk -v a="${b}-linux-${arch}" '$2 == a || $2 == "*"a { print $1; exit }' "$sums")
            got=$(sha256sum "$tmp" | awk '{print $1}')
            if [[ -n "$want" && "$want" != "$got" ]]; then rm -f "$tmp" "$sums"; bad "${b}: checksum mismatch against SHA256SUMS"; return 1; fi
        fi
        rm -f "$sums"
        run install -m 755 "$tmp" "${BIN}/${b}"; rm -f "$tmp"
        ok "${b} ${VERSION} installed from the release"
        return 0
    fi
    rm -f "$tmp"
    bad "${b} could not be downloaded from ${MIRROR} or GitHub"
    return 1
}

# 2. hitechcloud-rustcli: required since 3.4.0; this release's scripts call
#    commands only the 3.7.2 binary has (admin, domains-add, user-*...).
if ! { [[ -x "${BIN}/hitechcloud-rustcli" ]] && "${BIN}/hitechcloud-rustcli" ported 2>/dev/null | tr ' ' '\n' | grep -qx domains-add; }; then
    fetch hitechcloud-rustcli || warn "hitechcloud-rustcli is outdated; every command it carries is unavailable until the mirror is reachable (rerun: hitechcloudcli update)"
fi

# 3. The panels' Rust twins: required from 3.7.1. A twin that is present but
#    older than this release (no `routes` output / no page routes) is refreshed.
twin_ok() { [[ -x "${BIN}/$1" ]] && "${BIN}/$1" routes 2>/dev/null | grep -q ' page'; }
twins_failed=0
for twin in hitechcloud-rustpanel hitechcloud-rustadmin; do
    if twin_ok "$twin"; then ok "${twin} answers its route table"; continue; fi
    if fetch "$twin" && twin_ok "$twin"; then ok "${twin} answers its route table"; else bad "${twin} is missing or does not answer; the panel serves every route itself until it is installed"; twins_failed=1; fi
done

# 3b. The modules this server should have.
#
#     Two things changed under a running server in this release, and both are
#     invisible until something a customer clicks needs them:
#
#     - A Community server has never had a webmail installed. The panel's
#       Webmail button led nowhere, because nothing installed one. It is a
#       module now, so install it where mail is already running and no
#       webmail answers - and nowhere else: a server that has one keeps it,
#       and a server without mail has nothing to attach it to.
#     - Mail filters are enforced by the mail server from this release, which
#       needs ManageSieve. `native-mail install` is what writes that
#       configuration, and `native-module repair mail` is the supported way
#       to ask for it; it re-renders this module's own configuration and
#       restarts its units, and it never touches a mailbox.
#
#     Both are skipped on a server that already has them, both leave every
#     other module alone, and a failure here is a warning, not a rollback:
#     the panel is running either way, and `hitechcloudcli native-module`
#     can finish the job by hand.
#     Before any of that: the registry itself has to be on this server. The
#     updater refreshes the checkout but, until this release, never its
#     submodules - so configuration/ on a server that has updated a dozen
#     times is still the one it was installed from, and `native-module`
#     answers "no modules.json on this server" on every path it looks in.
#     Put the file where the feature catalogue beside it already lives.
REGISTRY=/etc/hitechcloud/hitechadmin/config/modules.json
if [[ ! -f "$REGISTRY" ]]; then
    for candidate in "${SRC}/configuration/hitechadmin/config/modules.json" \
                     /usr/local/hitechcloud/configuration/hitechadmin/config/modules.json; do
        [[ -f "$candidate" ]] || continue
        run mkdir -p "$(dirname "$REGISTRY")"
        run install -m 644 "$candidate" "$REGISTRY" && ok "module registry installed from the checkout" && break
    done
fi
if [[ ! -f "$REGISTRY" ]] && [[ "$DRY_RUN" != 1 ]]; then
    if curl -4fsSL --retry 2 --connect-timeout 20 "${MIRROR}/configuration/hitechadmin/config/modules.json" -o "$REGISTRY" 2>/dev/null \
       && [[ -s "$REGISTRY" ]]; then
        ok "module registry installed from the mirror"
    else
        rm -f "$REGISTRY"
    fi
fi

cli_has() { "${BIN}/hitechcloud-rustcli" --help 2>/dev/null | grep -q "^  $1 "; }
module_installed() { # module_installed <name>
    "${BIN}/hitechcloud-rustcli" native-module status "$1" --json 2>/dev/null \
        | grep -q '"installed"[[:space:]]*:[[:space:]]*true'
}
if [[ -f /etc/hitechcloud/native/native.conf ]] && cli_has native-module && [[ ! -f "$REGISTRY" ]]; then
    warn "no module registry on this server: the webmail and the mail module were left alone. Run 'hitechcloudcli update' again once ${MIRROR} serves configuration/hitechadmin/config/modules.json, then: hitechcloudcli native-module list"
elif [[ -f /etc/hitechcloud/native/native.conf ]] && cli_has native-module; then
    if module_installed mail; then
        if module_installed webmail || module_installed webmail-roundcube; then
            ok "a webmail is already installed"
        else
            run "${BIN}/hitechcloud-rustcli" native-module install webmail-roundcube \
                && ok "webmail installed for this server's mailboxes" \
                || warn "the webmail could not be installed; run: hitechcloudcli native-module install webmail-roundcube"
        fi
        if [[ -f /etc/dovecot/conf.d/99z-hitechcloud-managesieve.conf ]]; then
            ok "mail filters are already enforced at delivery"
        else
            run "${BIN}/hitechcloud-rustcli" native-module repair mail \
                && ok "mail filters are now enforced at delivery" \
                || warn "ManageSieve is not configured; run: hitechcloudcli native-module repair mail"
        fi
    else
        ok "no mail on this server; no mail module to change"
    fi
fi

# 4. Templates and the things every release refreshes.
if [[ -f /etc/hitechcloud/native/native.conf && -d "${SRC}/hitechcloudpanel/configuration-native" ]]; then
    run cp -a "${SRC}/hitechcloudpanel/configuration-native/." /etc/hitechcloud/native/templates/ 2>/dev/null && ok "Templates refreshed"
fi

# 5. Restart both panels and make sure they answer. The panel's own
#    /healthz (2083) and the admin's login page (2087) are the probes; the
#    ports come from the configuration when they were changed.
panel_port=$(hitechcloudcli port 2>/dev/null | tr -dc '0-9'); [[ -n "$panel_port" ]] || panel_port=2083
admin_port=$(hitechcloudcli admin port 2>/dev/null | tr -dc '0-9'); [[ -n "$admin_port" ]] || admin_port=2087
probe() { # probe <port> <path>
    for _ in $(seq 1 30); do
        curl -ksf --max-time 3 "https://127.0.0.1:$1$2" >/dev/null 2>&1 && return 0
        curl -sf  --max-time 3 "http://127.0.0.1:$1$2"  >/dev/null 2>&1 && return 0
        sleep 1
    done
    return 1
}
if [[ "$DRY_RUN" == 1 ]]; then echo "[3.7.2] would: restart hitechcloud hitechadmin and probe ${panel_port}/healthz and ${admin_port}/login"; exit 0; fi

systemctl daemon-reload
systemctl restart hitechcloud >/dev/null 2>&1 || true
systemctl is-enabled hitechadmin >/dev/null 2>&1 && systemctl restart hitechadmin >/dev/null 2>&1

healthy=1
probe "$panel_port" /healthz && ok "hitechcloud answers on ${panel_port}" || { bad "hitechcloud does not answer on ${panel_port}"; healthy=0; }
if systemctl is-enabled hitechadmin >/dev/null 2>&1; then
    probe "$admin_port" /login && ok "hitechadmin answers on ${admin_port}" || { bad "hitechadmin does not answer on ${admin_port}"; healthy=0; }
fi

if (( healthy == 0 )); then
    # Put back what this update changed, then what the regular updater
    # changed (<binary>.prev, kept by hitechcloudcli update since 3.7.1).
    bad "rolling back the binaries this update touched"
    for b in hitechcloud-rustcli hitechcloud-rustpanel hitechcloud-rustadmin; do
        [[ -f "${ROLLBACK}/${b}" ]] && install -m 755 "${ROLLBACK}/${b}" "${BIN}/${b}"
    done
    for b in hitechcloud hitechadmin; do
        [[ -f "${BIN}/${b}.prev" ]] && install -m 755 "${BIN}/${b}.prev" "${BIN}/${b}" && warn "${b}: previous build restored"
    done
    systemctl restart hitechcloud >/dev/null 2>&1 || true
    systemctl is-enabled hitechadmin >/dev/null 2>&1 && systemctl restart hitechadmin >/dev/null 2>&1
    probe "$panel_port" /healthz && warn "hitechcloud is back on the previous build" || bad "hitechcloud still does not answer: journalctl -u hitechcloud"
    echo "[3.7.2] The update was rolled back. Fix the cause and rerun: hitechcloudcli update" >&2
    exit 1
fi

# 5. The scheduled jobs. Every server installed by INSTALL.sh before this
#    release has an /etc/cron.d/hitechcloud with two header lines and no
#    jobs in it: no health monitor, no send-limit enforcement, no
#    status-page probes, no audit ingest, no backups, no statistics.
#    update.sh does this too; it is repeated here so that applying this
#    version by hand fixes it as well, and because it is the single
#    most consequential thing in the release.
#
#    Never destructive: it adds jobs whose command is absent and does not
#    touch, move or remove a line that is already there, so a schedule
#    edited from HiTechAdmin > Advanced > System CronJobs survives.
if [[ -x "${BIN}/hitechcloudcli" ]]; then
    if [[ ! -f /etc/hitechcloud/cron && -f "${SRC}/configuration/cron" ]]; then
        run install -m 0644 "${SRC}/configuration/cron" /etc/hitechcloud/cron
    fi
    if [[ "$DRY_RUN" == 1 ]]; then
        echo "[3.7.2] would: hitechcloudcli native-install --scheduled-jobs-only"
    elif "${BIN}/hitechcloudcli" native-install --scheduled-jobs-only; then
        :
    else
        warn "the scheduled jobs could not be reconciled; check /etc/cron.d/hitechcloud by hand"
    fi
fi

if (( twins_failed )); then
    warn "Version ${VERSION} applied without one of the Rust twins; rerun 'hitechcloudcli update' once the mirror is reachable"
    exit 0
fi
ok "Version ${VERSION} applied. Rust twins: $(${BIN}/hitechcloud-rustpanel routes | wc -l) panel routes, $(${BIN}/hitechcloud-rustadmin routes | wc -l) admin routes; CLI: $(${BIN}/hitechcloud-rustcli ported | wc -w) commands in Rust."
