#!/bin/bash

set -uEo pipefail
IFS=$'\n\t'

mkdir -p "$(dirname "${BASH_SOURCE[0]}")/logs/deploylogs"

_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || { echo "ERROR: Cannot resolve script directory." >&2; exit 1; }
readonly SCRIPT_DIR="$_SCRIPT_DIR"
unset _SCRIPT_DIR

cd "$SCRIPT_DIR" || { echo "ERROR: Cannot cd to $SCRIPT_DIR" >&2; exit 1; }
readonly BASE_URL_FILE="${SCRIPT_DIR}/installation_details/base_url.info"
readonly STATUS_FILE="${SCRIPT_DIR}/installation_details/status.info"
readonly NEW_VERSION_FILE="${SCRIPT_DIR}/installation_details/version.info"
readonly PREV_VERSION_FILE="${SCRIPT_DIR}/installation_details/previous_version.info"
readonly PREV_VERSION_TEMP_FILE="${SCRIPT_DIR}/installation_details/version_temp.info"
readonly APPLICATION_DIR="${SCRIPT_DIR}"
readonly BACKUP_DIR="${SCRIPT_DIR}/../vsaas_application/backup"
readonly DOWNLOAD_DIR="${SCRIPT_DIR}/temp/update_download"
readonly EXTRACT_DIR="${SCRIPT_DIR}/temp/update_extract"
readonly LOG_DIR="${SCRIPT_DIR}/logs/deploylogs"
readonly LOG_FILE="${LOG_DIR}/update.log"
readonly LOCK_FILE="/run/lock/vsaas_update.lock"
readonly MAX_BACKUPS=2
readonly WGET_TIMEOUT=120
readonly WGET_TRIES=3
readonly TLS_CHAIN_FILE="${SCRIPT_DIR}/installation_details/tls_chain.crt"
readonly SYSTEM_CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt"
readonly MAX_AIA_HOPS=5
readonly MIN_OPENSSL_VERSION="1.1.1"
readonly OPENSSL_PKG="openssl"

DEPLOYMENT_STARTED=0

rotate_log() {
    local max_bytes=$((5 * 1024 * 1024))
    local max_files=5
    [[ -f "$LOG_FILE" ]] || return 0
    local size
    size=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
    if (( size >= max_bytes )); then
        for i in $(seq $((max_files - 1)) -1 1); do
            [[ -f "${LOG_FILE}.${i}" ]] && mv "${LOG_FILE}.${i}" "${LOG_FILE}.$((i+1))"
        done
        mv "$LOG_FILE" "${LOG_FILE}.1"
        find "$(dirname "$LOG_FILE")" -name "$(basename "$LOG_FILE").*" \
            | sort -t. -k2 -n | tail -n +$((max_files + 1)) | xargs rm -f 2>/dev/null || true
    fi
}

log() {
    local level="${2:-INFO}"
    rotate_log
    printf "%s [%-5s] %s\n" "$(date +"%Y-%m-%d %H:%M:%S")" "$level" "$1" | tee -a "$LOG_FILE" || true
}

log_info()  { log "$1" "INFO";  }
log_warn()  { log "$1" "WARN";  }
log_error() { log "$1" "ERROR"; }

log_separator() {
    log "===================================================================================" "INFO"
}

_aia_url_from_cert() {
    openssl x509 -in "$1" -noout -text 2>/dev/null \
        | grep -m1 'CA Issuers - URI:' \
        | sed 's|.*CA Issuers - URI:||' | tr -d '[:space:]'
}

_cert_subject() { openssl x509 -in "$1" -noout -subject 2>/dev/null | sed 's/^subject=//'; }
_cert_issuer()  { openssl x509 -in "$1" -noout -issuer  2>/dev/null | sed 's/^issuer=//';  }

_is_self_signed() {
    [[ "$(_cert_subject "$1")" == "$(_cert_issuer "$1")" ]]
}

_STRICT_VERIFY_ARGS=()
_init_strict_verify() {
    local probe
    for probe in -no-CAfile -no-CApath -no-CAstore; do
        if openssl verify -help 2>&1 | grep -q -- "$probe"; then
            _STRICT_VERIFY_ARGS+=("$probe")
        fi
    done
}
_init_strict_verify

_verify_leaf() {
    local cafile="$1" leaf="$2" untrusted="${3:-}"
    local -a args=(verify "${_STRICT_VERIFY_ARGS[@]}" -CAfile "$cafile")
    [[ -n "$untrusted" && -s "$untrusted" ]] && args+=(-untrusted "$untrusted")
    openssl "${args[@]}" "$leaf" 2>&1 | grep -v ': OK$' || true
}

_verify_ok() {
    local out
    out=$(_verify_leaf "$@")
    [[ -z "$out" ]]
}

_fetch_aia_cert() {
    local url="$1" out="$2" raw
    raw=$(mktemp)
    if ! wget --no-verbose --timeout=30 --tries=3 -O "$raw" "$url" 2>/dev/null; then
        rm -f "$raw"; return 1
    fi
    if   openssl x509 -inform DER -in "$raw" -out "$out" 2>/dev/null; then :
    elif openssl x509 -inform PEM -in "$raw" -out "$out" 2>/dev/null; then :
    else rm -f "$raw"; return 1
    fi
    rm -f "$raw"
}

ensure_tls_chain() {
    local TLS_HOSTNAME
    TLS_HOSTNAME=$(tr -d '[:space:]' < "$BASE_URL_FILE" 2>/dev/null \
        | sed 's|https\?://||' | cut -d/ -f1)
    local base_url_raw
    base_url_raw=$(tr -d '[:space:]' < "$BASE_URL_FILE" 2>/dev/null || true)
    if [[ "$base_url_raw" != https://* ]]; then
        safe_error_exit "Base URL must start with https:// (found: '${base_url_raw}'). Refusing to fetch the update over an unverified transport."
    fi
    if [[ -z "$TLS_HOSTNAME" ]]; then
        safe_error_exit "Cannot derive hostname from BASE_URL_FILE for TLS chain build."
    fi

    local work
    work=$(mktemp -d)
    trap '[[ -n "${work:-}" ]] && rm -rf "$work"' RETURN

    local served="$work/served.pem" leaf="$work/leaf.pem" inter="$work/intermediates.pem"

    local attempt
    for attempt in 1 2 3; do
        set +o pipefail
        openssl s_client -connect "${TLS_HOSTNAME}:443" -servername "$TLS_HOSTNAME" \
            -showcerts </dev/null 2>/dev/null \
            | awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/' > "$served"
        set -o pipefail
        [[ -s "$served" ]] && break
        log_warn "TLS handshake attempt ${attempt}/3 to $TLS_HOSTNAME returned no certificate; retrying..."
        sleep $(( attempt * 3 ))
    done

    if [[ ! -s "$served" ]]; then
        safe_error_exit "Failed to retrieve certificate chain from $TLS_HOSTNAME after 3 attempts — server unreachable or returned no cert."
    fi

    csplit -s -z -f "$work/c_" "$served" '/-----BEGIN CERTIFICATE-----/' '{*}'
    mv "$work/c_00" "$leaf"
    : > "$inter"
    local f
    for f in "$work"/c_*; do [[ -f "$f" ]] && cat "$f" >> "$inter"; done

    local served_inters
    served_inters=$(grep -c 'BEGIN CERTIFICATE' "$inter" || true)
    log_info "Leaf cert subject: $(_cert_subject "$leaf")"
    log_info "Leaf cert issuer:  $(_cert_issuer "$leaf")"
    log_info "Server presented ${served_inters} intermediate cert(s)."

    if [[ -s "$TLS_CHAIN_FILE" ]]; then
        if _verify_ok "$TLS_CHAIN_FILE" "$leaf" "$inter"; then
            log_info "Cached TLS chain validates current server cert: $TLS_CHAIN_FILE"
            return
        fi
        log_warn "Cached TLS chain no longer validates the server cert (intermediate rotated or expired) — rebuilding."
        rm -f "$TLS_CHAIN_FILE"
    fi

    log_info "Building TLS chain for: $TLS_HOSTNAME"

    if ! _verify_ok "$SYSTEM_CA_BUNDLE" "$leaf" "$inter"; then
        log_warn "Served chain does not reach a trusted root: $(_verify_leaf "$SYSTEM_CA_BUNDLE" "$leaf" "$inter" | head -1)"
        log_info "Walking AIA to complete the chain (max ${MAX_AIA_HOPS} hops)..."

        local top="$leaf"
        if (( served_inters > 0 )); then
            top="$work/top.pem"
            awk 'BEGIN{RS="-----END CERTIFICATE-----\n"} NF{last=$0} END{printf "%s-----END CERTIFICATE-----\n", last}' "$inter" > "$top"
        fi

        local hop=0 url next
        while (( hop < MAX_AIA_HOPS )); do
            if _is_self_signed "$top"; then
                log_error "Reached self-signed cert '$(_cert_subject "$top")' that is NOT in the system CA bundle."
                log_error "The edge OS ca-certificates package is too old for this CA hierarchy. Update the OS image or add the root to /usr/local/share/ca-certificates/."
                safe_error_exit "TLS chain cannot be anchored to the system trust store."
            fi
            url=$(_aia_url_from_cert "$top")
            if [[ -z "$url" ]]; then
                log_error "No AIA URL in '$(_cert_subject "$top")' and its issuer '$(_cert_issuer "$top")' is not in the system CA bundle."
                log_error "The edge OS ca-certificates package is too old for this CA hierarchy. Update the OS image or add the root to /usr/local/share/ca-certificates/."
                safe_error_exit "TLS chain cannot be anchored to the system trust store."
            fi
            next="$work/aia_${hop}.pem"
            log_info "  hop $((hop+1)): fetching issuer of '$(_cert_subject "$top")' from $url"
            _fetch_aia_cert "$url" "$next" \
                || safe_error_exit "Failed to fetch/decode certificate from AIA URL: $url"
            log_info "  got '$(_cert_subject "$next")' (issuer: $(_cert_issuer "$next"))"
            cat "$next" >> "$inter"
            top="$next"
            hop=$((hop+1))
            _verify_ok "$SYSTEM_CA_BUNDLE" "$leaf" "$inter" && break
        done

        if ! _verify_ok "$SYSTEM_CA_BUNDLE" "$leaf" "$inter"; then
            log_error "Chain still fails after ${hop} AIA hop(s): $(_verify_leaf "$SYSTEM_CA_BUNDLE" "$leaf" "$inter" | head -1)"
            safe_error_exit "Unable to build a trusted TLS chain for $TLS_HOSTNAME."
        fi
    fi

    local staged="$work/chain.staged"
    if ! cat "$SYSTEM_CA_BUNDLE" > "$staged"; then
        safe_error_exit "Cannot read system CA bundle: $SYSTEM_CA_BUNDLE"
    fi
    if [[ -s "$inter" ]] && ! cat "$inter" >> "$staged"; then
        safe_error_exit "Failed to append intermediates while building TLS chain."
    fi
    if ! _verify_ok "$staged" "$leaf" "$inter"; then
        safe_error_exit "Freshly built TLS chain does not validate the server certificate — refusing to install it."
    fi
    mv -f "$staged" "$TLS_CHAIN_FILE" \
        || safe_error_exit "Cannot install TLS chain file: $TLS_CHAIN_FILE"

    log_info "TLS chain built and verified against system CA bundle."
    local chain_desc=""
    if [[ -s "$inter" ]]; then
        csplit -s -z -f "$work/d_" "$inter" '/-----BEGIN CERTIFICATE-----/' '{*}'
        for f in "$work"/d_*; do
            [[ -f "$f" ]] && chain_desc+=" <- $(_cert_subject "$f")"
        done
    fi
    log_info "Chain: $(_cert_subject "$leaf")${chain_desc} <- (system root)"
}

_version_ge() {
    [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]]
}

_installed_openssl_version() {
    local line
    line=$(openssl version 2>/dev/null) || return 1
    [[ "$line" == OpenSSL\ * ]] || return 1
    awk '{print $2}' <<< "$line"
}

_candidate_openssl_version() {
    apt-cache policy "$OPENSSL_PKG" 2>/dev/null \
        | awk '/Candidate:/{print $2}' \
        | sed 's/^[0-9]*://; s/[-+~].*//'
}

_openssl_is_usable() {
    local v
    v=$(_installed_openssl_version) || return 1
    [[ -n "$v" ]] || return 1
    _version_ge "$v" "$MIN_OPENSSL_VERSION" || return 1
    openssl x509 -help  </dev/null &>/dev/null || return 1
    openssl verify -help </dev/null &>/dev/null || return 1
    return 0
}

_reinstall_openssl_surgically() {
    log_warn "Purging ${OPENSSL_PKG} and reinstalling from the repository..."

    if ! sudo apt-get install -y -qq -d --reinstall "$OPENSSL_PKG"; then
        safe_error_exit "Cannot stage the ${OPENSSL_PKG} package for reinstall — refusing to purge without a replacement on disk."
    fi

    if ! sudo dpkg --purge --force-depends "$OPENSSL_PKG" >/dev/null 2>&1; then
        log_warn "dpkg purge of ${OPENSSL_PKG} reported errors — continuing to reinstall."
    fi

    if ! sudo apt-get install -y -qq "$OPENSSL_PKG"; then
        sudo apt-get install -y -qq -f || true
        sudo apt-get install -y -qq "$OPENSSL_PKG" \
            || safe_error_exit "Reinstall of ${OPENSSL_PKG} failed. The appliance now has no openssl binary — restore it manually with: apt-get install --reinstall openssl"
    fi

    if ! dpkg -s ca-certificates >/dev/null 2>&1; then
        log_warn "ca-certificates was removed alongside openssl — restoring it."
        sudo apt-get install -y -qq ca-certificates \
            || safe_error_exit "ca-certificates could not be restored; the TLS trust store is incomplete."
    fi
    sudo update-ca-certificates --fresh >/dev/null 2>&1 || true
}

ensure_openssl() {
    local current candidate

    if _openssl_is_usable; then
        log_info "openssl $(_installed_openssl_version) satisfies the minimum (${MIN_OPENSSL_VERSION})."
        return 0
    fi

    current=$(_installed_openssl_version 2>/dev/null || true)
    if [[ -z "$current" ]]; then
        log_warn "openssl is missing or not a usable OpenSSL build."
    elif ! _version_ge "$current" "$MIN_OPENSSL_VERSION"; then
        log_warn "openssl ${current} is too old (minimum required: ${MIN_OPENSSL_VERSION})."
    else
        log_warn "openssl ${current} meets the minimum version but is not functional."
    fi

    sudo NEEDRESTART_MODE=a apt-get update -y -qq || true

    candidate=$(_candidate_openssl_version)
    if [[ -z "$candidate" ]]; then
        safe_error_exit "No ${OPENSSL_PKG} candidate is available from the configured apt sources."
    fi
    if ! _version_ge "$candidate" "$MIN_OPENSSL_VERSION"; then
        safe_error_exit "The apt candidate for ${OPENSSL_PKG} is ${candidate}, below the required ${MIN_OPENSSL_VERSION}. Update the OS image or apt sources; removing the current openssl would leave the appliance worse off."
    fi
    log_info "Repository offers ${OPENSSL_PKG} ${candidate}; repairing."

    sudo apt-get install -y -qq "$OPENSSL_PKG" >/dev/null 2>&1 || true
    if _openssl_is_usable; then
        log_info "openssl repaired by install: $(_installed_openssl_version)"
        return 0
    fi

    sudo apt-get install -y -qq --only-upgrade "$OPENSSL_PKG" >/dev/null 2>&1 || true
    if _openssl_is_usable; then
        log_info "openssl repaired by upgrade: $(_installed_openssl_version)"
        return 0
    fi

    sudo apt-get install -y -qq --reinstall "$OPENSSL_PKG" >/dev/null 2>&1 || true
    if _openssl_is_usable; then
        log_info "openssl repaired by reinstall: $(_installed_openssl_version)"
        return 0
    fi

    _reinstall_openssl_surgically

    if _openssl_is_usable; then
        log_info "openssl restored: $(_installed_openssl_version)"
        return 0
    fi

    safe_error_exit "openssl is still unusable after purge and reinstall (found: $(_installed_openssl_version 2>/dev/null || echo none)). Manual intervention required."
}

ensure_dependencies() {
    log_info "Checking required dependencies..."
    local pkgs=()

    command -v wget   &>/dev/null || pkgs+=(wget)
    command -v zip    &>/dev/null || pkgs+=(zip)
    command -v unzip  &>/dev/null || pkgs+=(unzip)
    command -v flock  &>/dev/null || pkgs+=(util-linux)
    command -v rsync  &>/dev/null || pkgs+=(rsync)
    command -v docker &>/dev/null || { log_error "docker is required but not installed. Aborting."; exit 1; }
    pkgs+=(ca-certificates)

    if [[ ${#pkgs[@]} -gt 0 ]]; then
        log_info "Installing missing packages: ${pkgs[*]}"
        sudo NEEDRESTART_MODE=a apt-get update -y -qq
        sudo apt-get install -y -qq "${pkgs[@]}"
    fi

    ensure_openssl

    log_info "Refreshing system CA trust store..."
    sudo update-ca-certificates --fresh

    ensure_tls_chain

    sudo apt-get autoremove -y -qq
    log_info "All dependencies satisfied."
}

cleanup() {
    log_info "Cleaning up temporary directories."
    rm -rf "$DOWNLOAD_DIR" "$EXTRACT_DIR"
}

reset_workspace() {
    local real_app
    real_app=$(realpath --canonicalize-existing "$APPLICATION_DIR" 2>/dev/null) \
        || error_exit "Cannot resolve APPLICATION_DIR: $APPLICATION_DIR"

    local d real_d
    for d in "$DOWNLOAD_DIR" "$EXTRACT_DIR"; do
        [[ -e "$d" ]] || continue
        real_d=$(realpath --canonicalize-existing "$d" 2>/dev/null) || continue
        case "$real_d" in
            "$real_app"/temp/*)
                log_info "Clearing stale workspace from a previous run: $real_d"
                rm -rf -- "$real_d"
                ;;
            *)
                log_warn "Refusing to clear workspace path outside the install tree: $real_d"
                ;;
        esac
    done

    mkdir -p "$DOWNLOAD_DIR" "$EXTRACT_DIR" \
        || error_exit "Cannot recreate workspace directories under ${APPLICATION_DIR}/temp."
}

restore_ownership() {
    local uid gid
    if [[ -n "${SUDO_UID:-}" && -n "${SUDO_GID:-}" ]]; then
        uid="$SUDO_UID"; gid="$SUDO_GID"
    else
        uid=$(stat -c %u "$APPLICATION_DIR" 2>/dev/null || id -u)
        gid=$(stat -c %g "$APPLICATION_DIR" 2>/dev/null || id -g)
    fi
    sudo chown -R "${uid}":"${gid}" "$APPLICATION_DIR" \
        || log_warn "chown of $APPLICATION_DIR to ${uid}:${gid} failed — check ownership manually."
}

rollback() {
    log_warn "Initiating rollback..."
    set_status "Rollback in progress"

    local old_version
    old_version="$(cat "$PREV_VERSION_FILE" 2>/dev/null | tr -d '[:space:]')" || old_version="unknown"

    if [[ -n "${BACKUP_ZIP:-}" && -f "$BACKUP_ZIP" ]] && ! unzip -qt "$BACKUP_ZIP" >/dev/null 2>&1; then
        log_error "Backup archive is corrupt: $BACKUP_ZIP — refusing to restore from it. Manual intervention required."
    elif [[ -n "${BACKUP_ZIP:-}" && -f "$BACKUP_ZIP" ]]; then
        log_info "Restoring backup from: $BACKUP_ZIP"
        if unzip -o "$BACKUP_ZIP" -d "$APPLICATION_DIR" >/dev/null; then
            restore_ownership
            log_info "Backup restored successfully."
        else
            log_error "Failed to restore backup. Manual intervention required."
        fi
    else
        log_error "No backup found at '${BACKUP_ZIP:-<unset>}'. Cannot restore automatically."
    fi

    log_info "Starting Docker containers after rollback..."
    if sudo docker compose up -d; then
        log_info "Docker containers are running."
    else
        log_error "Failed to start Docker containers after rollback. Manual intervention required."
    fi

    if [[ -n "$old_version" && "$old_version" != "unknown" ]]; then
        echo "$old_version" > "$NEW_VERSION_FILE"
        log_info "Version file reverted to: $old_version"
    fi

    set_status "Rollback completed"
    log_warn "Rollback finished."
}

error_exit() {
    log_error "$1"
    if (( DEPLOYMENT_STARTED == 0 )); then
        log_warn "Failure occurred before deployment began — no rollback needed; target version left unchanged."
        set_status "Update failed (no changes made)"
        cleanup
        exit 1
    fi
    rollback
    cleanup
    exit 1
}

safe_error_exit() {
    log_error "$1"
    set_status "Update failed (early error)" 2>/dev/null || true
    cleanup 2>/dev/null || true
    exit 1
}

on_signal() {
    trap '' INT TERM HUP
    log_error "Interrupted by signal — aborting update."
    if (( DEPLOYMENT_STARTED == 1 )); then
        rollback
    else
        set_status "Update aborted (interrupted)"
    fi
    cleanup
    exit 130
}

trap 'error_exit "Unexpected error on line ${LINENO}. Command: ${BASH_COMMAND}"' ERR
trap on_signal INT TERM HUP

set_status() {
    mkdir -p "$(dirname "$STATUS_FILE")" 2>/dev/null || true
    echo "$1" > "$STATUS_FILE" 2>/dev/null || true
}

retain_last_n_backups() {
    local n="${1:-$MAX_BACKUPS}"
    log_info "Enforcing backup retention policy (keeping last $n)..."

    local -a entries=()
    local entry
    while IFS= read -r -d '' entry; do
        entries+=("$entry")
    done < <(find "$BACKUP_DIR" -maxdepth 1 -type f -name '*.zip' -printf '%T@\t%p\0' 2>/dev/null | sort -z -n)

    local count=${#entries[@]}

    if (( count > n )); then
        log_info "Found $count backups; removing oldest $((count - n))..."
        local i removed=0
        for (( i = 0; i < count - n; i++ )); do
            if rm -f -- "${entries[$i]#*$'\t'}"; then
                removed=$((removed + 1))
            else
                log_warn "Could not remove old backup: ${entries[$i]#*$'\t'}"
            fi
        done
        log_info "Old backups removed ($removed)."
    else
        log_info "Backup count ($count) is within limit. No removal needed."
    fi
}

fetch_url() {
    local url="$1" dest="$2"
    if wget --no-verbose --ca-certificate="$TLS_CHAIN_FILE" \
            --timeout="$WGET_TIMEOUT" --tries="$WGET_TRIES" -O "$dest" "$url"; then
        return 0
    fi
    log_warn "Download with built TLS chain failed — retrying against the system trust store."
    if wget --no-verbose --timeout="$WGET_TIMEOUT" --tries="$WGET_TRIES" -O "$dest" "$url"; then
        log_warn "System trust store succeeded; discarding the stale chain file so it is rebuilt next run."
        rm -f "$TLS_CHAIN_FILE"
        return 0
    fi
    return 1
}

pull_compose_images() {
    local compose_file="${APPLICATION_DIR}/docker-compose.yml"
    if [[ ! -f "$compose_file" ]]; then
        error_exit "docker-compose.yml not found at: $compose_file"
    fi

    local images
    images=$(grep -E '^[[:space:]]*image:[[:space:]]' "$compose_file" | awk '{print $2}' || true)

    if [[ -z "$images" ]]; then
        log_warn "No images found in docker-compose.yml. Skipping pull."
        return
    fi

    log_info "Pulling images defined in docker-compose.yml..."
    while IFS= read -r image; do
        [[ -z "$image" ]] && continue
        log_info "Pulling: $image"
        if ! sudo docker pull "$image"; then
            log_warn "Failed to pull '$image' — will use existing local image if available."
        fi
    done <<< "$images"

    log_info "Image pull phase complete."
}

main() {
    mkdir -p "$DOWNLOAD_DIR" "$EXTRACT_DIR" "$BACKUP_DIR" "$LOG_DIR" \
             "$(dirname "$STATUS_FILE")" "$(dirname "$NEW_VERSION_FILE")" \
        || error_exit "Cannot create working directories (checked: $DOWNLOAD_DIR, $BACKUP_DIR)."

    exec 200>"$LOCK_FILE" || error_exit "Cannot open update lock: $LOCK_FILE"
    if ! flock -n 200; then
        log_warn "Another update is already running (lock held on $LOCK_FILE) — exiting."
        exit 0
    fi

    log_separator
    log_info "Update script started at $(date)."
    log_separator

    reset_workspace

    ensure_dependencies

    [[ -f "$NEW_VERSION_FILE" ]] || error_exit "Version file not found: $NEW_VERSION_FILE"
    VERSION=$(tr -d '[:space:]' < "$NEW_VERSION_FILE")
    [[ -n "$VERSION" ]]          || error_exit "Version file is empty: $NEW_VERSION_FILE"
    [[ "$VERSION" =~ ^[0-9]+([.][0-9]+)*$ ]] \
        || error_exit "Version string is invalid (must be digits and dots only): '$VERSION'"
    log_info "Target version: $VERSION"

    if [[ -f "$PREV_VERSION_FILE" ]]; then
        PREV_VERSION=$(tr -d '[:space:]' < "$PREV_VERSION_FILE")
        [[ -n "$PREV_VERSION" ]] || error_exit "Previous version file is empty: $PREV_VERSION_FILE"
        [[ "$PREV_VERSION" =~ ^[0-9]+([.][0-9]+)*$ ]] \
            || error_exit "Previous version string is invalid (must be digits and dots only): '$PREV_VERSION'"
        cp "$PREV_VERSION_FILE" "$PREV_VERSION_TEMP_FILE" \
            || log_warn "Could not write $PREV_VERSION_TEMP_FILE — continuing."
    else
        log_warn "No previous version file found; rollback version will be 'unknown'."
        PREV_VERSION="unknown_version"
    fi

    [[ -f "$BASE_URL_FILE" ]] || error_exit "Base URL file not found: $BASE_URL_FILE"
    BASE_URL=$(tr -d '[:space:]' < "$BASE_URL_FILE")
    BASE_URL="${BASE_URL%/}"
    [[ -n "$BASE_URL" ]]      || error_exit "Base URL file is empty."
    [[ "$BASE_URL" == https://* ]] \
        || error_exit "Base URL must start with https:// (found: '$BASE_URL'). Refusing to fetch the update over an unverified transport."

    readonly ZIP_FILE="${VERSION}.zip"
    readonly DOWNLOAD_URL="${BASE_URL}/${ZIP_FILE}"
    log_info "Download URL: $DOWNLOAD_URL"

    set_status "Update in progress"

    log_info "Downloading $ZIP_FILE..."
    fetch_url "$DOWNLOAD_URL" "${DOWNLOAD_DIR}/${ZIP_FILE}" \
        || error_exit "Download failed from: $DOWNLOAD_URL"
    log_info "Download complete."

    [[ -f "${DOWNLOAD_DIR}/${ZIP_FILE}" ]] \
        || error_exit "Downloaded file missing: ${DOWNLOAD_DIR}/${ZIP_FILE}"

    readonly CHECKSUM_FILE="${ZIP_FILE}.sha256"
    readonly CHECKSUM_URL="${BASE_URL}/${CHECKSUM_FILE}"
    readonly CHECKSUM_LOCAL="${DOWNLOAD_DIR}/${CHECKSUM_FILE}"

    log_info "Probing for checksum file: $CHECKSUM_URL"
    if wget --no-verbose --ca-certificate="$TLS_CHAIN_FILE" --timeout="$WGET_TIMEOUT" \
            --tries=1 --spider "$CHECKSUM_URL" 2>/dev/null \
       || wget --no-verbose --timeout="$WGET_TIMEOUT" --tries=1 --spider "$CHECKSUM_URL" 2>/dev/null; then

        log_info "Checksum file found — downloading..."
        fetch_url "$CHECKSUM_URL" "$CHECKSUM_LOCAL" \
            || error_exit "Checksum download failed from: $CHECKSUM_URL"

        log_info "Verifying SHA-256 checksum..."
        local expected_hash actual_hash
        expected_hash=$(head -n1 "$CHECKSUM_LOCAL" | awk '{print $1}' | tr -d '[:space:]')
        actual_hash=$(sha256sum "${DOWNLOAD_DIR}/${ZIP_FILE}" | awk '{print $1}')

        if [[ "$expected_hash" != "$actual_hash" ]]; then
            error_exit "Checksum mismatch for ${ZIP_FILE}. Expected: ${expected_hash}  Got: ${actual_hash}"
        fi
        log_info "Checksum verified: $actual_hash"
    else
        log_warn "Checksum file not available on server — skipping integrity check."
        log_warn "RECOMMENDED: publish ${CHECKSUM_FILE} alongside the zip on the update server."
    fi

    log_info "Extracting ${ZIP_FILE}..."
    unzip -q -o "${DOWNLOAD_DIR}/${ZIP_FILE}" -d "$EXTRACT_DIR" \
        || error_exit "Extraction failed: ${DOWNLOAD_DIR}/${ZIP_FILE}"
    log_info "Extraction complete."

    readonly FULL_EXTRACT_DIR="${EXTRACT_DIR}/${VERSION}"
    [[ -d "$FULL_EXTRACT_DIR" ]] \
        || error_exit "Expected extract directory not found: $FULL_EXTRACT_DIR"

    DEPLOYMENT_STARTED=1

    log_info "Stopping Docker services..."
    sudo docker compose down \
        || log_warn "docker compose down returned non-zero (services may already be stopped)."

    readonly BACKUP_ZIP="${BACKUP_DIR}/backup_${PREV_VERSION}.zip"
    readonly BACKUP_STAGED="${BACKUP_ZIP}.partial"

    if [[ -e "$BACKUP_ZIP" ]]; then
        log_info "An existing backup is present and will be overwritten: $BACKUP_ZIP"
    fi

    log_info "Creating backup: $BACKUP_ZIP"
    cd "$APPLICATION_DIR" || error_exit "Cannot cd to APPLICATION_DIR: $APPLICATION_DIR"

    rm -f -- "$BACKUP_STAGED"

    local zip_rc=0
    zip -q -r "$BACKUP_STAGED" . \
        --exclude='./temp/*' \
        --exclude='./logs/*' \
        || zip_rc=$?

    case "$zip_rc" in
        0)
            ;;
        18)
            log_warn "Some files could not be read and were skipped during backup (zip exit 18). This is normal when background jobs rewrite files; the archive itself is still valid."
            ;;
        12)
            log_warn "zip reported nothing to archive (exit 12)."
            ;;
        *)
            rm -f -- "$BACKUP_STAGED"
            error_exit "Backup creation failed (zip exit ${zip_rc})."
            ;;
    esac

    [[ -s "$BACKUP_STAGED" ]] \
        || { rm -f -- "$BACKUP_STAGED"; error_exit "Backup archive was not created: $BACKUP_STAGED"; }

    if ! unzip -qt "$BACKUP_STAGED" >/dev/null 2>&1; then
        rm -f -- "$BACKUP_STAGED"
        error_exit "Backup archive failed its integrity check — refusing to keep a corrupt backup."
    fi

    mv -f -- "$BACKUP_STAGED" "$BACKUP_ZIP" \
        || { rm -f -- "$BACKUP_STAGED"; error_exit "Cannot move backup into place: $BACKUP_ZIP"; }

    log_info "Backup created and verified: $BACKUP_ZIP"

    retain_last_n_backups "$MAX_BACKUPS"

    log_info "Deploying new version files..."
    rsync -a -I \
        "${FULL_EXTRACT_DIR}/" \
        "${APPLICATION_DIR}/" \
        || error_exit "rsync deployment failed."
    log_info "Deployment complete."

    local deployed_version
    deployed_version=$(tr -d '[:space:]' < "$NEW_VERSION_FILE" 2>/dev/null || true)
    if [[ "$deployed_version" != "$VERSION" ]]; then
        log_warn "Payload shipped version.info='${deployed_version:-<empty>}' but the deployed release is '${VERSION}'. Correcting the local version records to '${VERSION}'."
        printf '%s\n' "$VERSION" > "$NEW_VERSION_FILE" \
            || error_exit "Cannot write the authoritative version to $NEW_VERSION_FILE"
    fi

    restore_ownership

    cleanup

    pull_compose_images

    local settings_script="${APPLICATION_DIR}/apply_settings.sh"
    if [[ -f "$settings_script" ]]; then
        local real_settings real_appdir
        real_settings=$(realpath "$settings_script")
        real_appdir=$(realpath "$APPLICATION_DIR")
        if [[ "$real_settings" != "${real_appdir}"/* ]]; then
            error_exit "apply_settings.sh resolved outside APPLICATION_DIR: $real_settings"
        fi
        log_info "Applying additional settings..."
        sudo bash "$real_settings" \
            || log_warn "apply_settings.sh returned non-zero; review output manually."
    else
        log_warn "apply_settings.sh not found — skipping."
    fi

    log_info "Starting Docker containers..."
    sudo docker compose up -d || error_exit "Failed to start Docker containers."
    log_info "Docker containers are running."

    log_info "Waiting 20 seconds before pruning unused Docker objects..."
    sleep 20

    log_info "Pruning unused Docker system objects..."
    sudo docker system prune -af \
        || log_warn "docker system prune returned non-zero."

    cp "$NEW_VERSION_FILE" "$PREV_VERSION_FILE" \
        || log_error "Could not update $PREV_VERSION_FILE — the rollback target is now stale. Fix manually."

    set_status "Update completed successfully"
    log_separator
    log_info "Update to version '$VERSION' completed successfully."
    log_separator
}

main "$@"
