#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
ORCH_NODES="$SCRIPT_DIR/orchestrator-nodes.tsv"
ROUTES="$SCRIPT_DIR/haproxy-required-lines.txt"
VIP_POLICY="$SCRIPT_DIR/vip-policy.tsv"
REMOTE_EVIDENCE_ROOT=/var/backups/zhct-q210-251/evidence
BACKUP_ROOT=/var/backups/zhct-q210-251/files
SECRET_CONFIRMATION=GLOBAL_SAME_SOURCE_CONFIRMED_WITHOUT_VALUE_DISCLOSURE
ORIGINAL_ARGS=("$@")

usage() {
    cat <<'USAGE'
Usage:
  q210-251-nginx-ha-control.sh --static-validate
  q210-251-nginx-ha-control.sh MODE --task q210|q250 --node nginx01|nginx02|redis01
  q210-251-nginx-ha-control.sh MODE --task q251 --node nginx01|nginx02 --vip front|mqtt|db-proxy
MODE: preflight|execute|verify|rollback|archive
USAGE
}

die() { printf 'ERROR: %s\n' "$*" >&2; return 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }
assert_pass() { [[ "${!1:-}" == PASS ]] || die "$1=PASS is required"; }

mode=${1:-}
shift || true
task=
node=
vip=
if [[ "$mode" != --static-validate ]]; then
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --task) task=${2:-}; shift 2 ;;
            --node) node=${2:-}; shift 2 ;;
            --vip) vip=${2:-}; shift 2 ;;
            *) usage; exit 2 ;;
        esac
    done
fi
case "$mode" in --static-validate|preflight|execute|verify|rollback|archive) ;; *) usage; exit 2 ;; esac

if [[ "$mode" == --static-validate ]]; then
    [[ -z "$task$node$vip" ]] || { usage; exit 2; }
    task=static
    node=static
elif [[ "$task" == q210 && "$node" =~ ^(nginx01|nginx02|redis01)$ && -z "$vip" ]]; then
    :
elif [[ "$task" == q250 && "$node" =~ ^(nginx01|nginx02)$ && -z "$vip" ]]; then
    :
elif [[ "$task" == q251 && "$node" =~ ^(nginx01|nginx02)$ && "$vip" =~ ^(front|mqtt|db-proxy)$ ]]; then
    :
else
    usage
    exit 2
fi

case "$node" in
    nginx01) target_host=sqjcnginx01; target_ip=10.2.203.102; target_role=nginx; sequence=1 ;;
    nginx02) target_host=sqjcnginx02; target_ip=10.2.203.103; target_role=nginx; sequence=2 ;;
    redis01) target_host=sqjcredis01; target_ip=10.2.203.112; target_role=redis; sequence=3 ;;
    *) target_host=LOCAL_STATIC_ONLY; target_ip=LOCAL_STATIC_ONLY; target_role=static; sequence=0 ;;
esac

run_id=${HA_RUN_ID:-}
if [[ "$mode" == --static-validate ]]; then
    run_id=${run_id:-ha-static-validate}
    evidence_root=${HA_EVIDENCE_ROOT:-$(mktemp -d "${TMPDIR:-/tmp}/q210-251-static.XXXXXX")}
else
    [[ "$run_id" =~ ^q(210|250|251)-[A-Za-z0-9._+-]+$ ]] || die 'invalid or missing HA_RUN_ID'
    [[ "$run_id" == "$task"-* ]] || die 'HA_RUN_ID task prefix mismatch'
    evidence_root=${HA_EVIDENCE_ROOT:-$REMOTE_EVIDENCE_ROOT}
fi
evidence_dir="$evidence_root/$run_id/$node"
mkdir -p "$evidence_dir"
stamp=$(date -u +%Y%m%dT%H%M%SZ)
log=${HA_LOG_FILE:-"$evidence_dir/$stamp-$task-${mode#--}${vip:+-$vip}.log"}
rc_file=${HA_RC_FILE:-"$evidence_dir/$stamp-$task-${mode#--}${vip:+-$vip}.rc"}
if [[ ${HA_LOG_WRAPPED:-0} != 1 ]]; then
    export HA_LOG_WRAPPED=1 HA_LOG_FILE="$log" HA_RC_FILE="$rc_file"
    export HA_EVIDENCE_ROOT="$evidence_root" HA_RUN_ID="$run_id"
    set +e
    "$SCRIPT_PATH" "${ORIGINAL_ARGS[@]}" 2>&1 | tee -a "$log"
    wrapped_rc=${PIPESTATUS[0]}
    exit "$wrapped_rc"
fi

finish() {
    local rc=$?
    {
        printf 'HA_MODE_END=%s\n' "$mode"
        printf 'HA_TASK=%s\n' "$task"
        printf 'HA_NODE=%s\n' "$node"
        printf 'HA_VIP=%s\n' "${vip:-NONE}"
        printf 'HA_END_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
        printf 'HA_TARGET_HOST=%s\n' "$target_host"
        printf 'HA_TARGET_IP=%s\n' "$target_ip"
        printf 'HA_RC=%s\n' "$rc"
        if [[ "$rc" -eq 0 ]]; then
            printf 'HA_JUDGMENT=MODE_PASS_NOT_GLOBAL_TASK_PASS\n'
        else
            printf 'HA_JUDGMENT=FAIL_CLOSED\n'
        fi
        printf 'HA_ROLLBACK_STATE=EXPLICIT_ROLLBACK_ONLY_NO_AUTOMATIC_OVERWRITE\n'
    } | tee -a "$rc_file"
    return "$rc"
}
trap finish EXIT

printf 'HA_MODE_BEGIN=%s\n' "$mode"
printf 'HA_TASK=%s\n' "$task"
printf 'HA_NODE=%s\n' "$node"
printf 'HA_VIP=%s\n' "${vip:-NONE}"
printf 'HA_START_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'HA_TARGET_HOST=%s\n' "$target_host"
printf 'HA_TARGET_IP=%s\n' "$target_ip"
printf 'HA_RUN_ID=%s\n' "$run_id"

static_validate() {
    bash -n "$SCRIPT_PATH"
    python3 - "$SCRIPT_DIR/q251-render-keepalived.py" <<'PY'
import ast, pathlib, sys
ast.parse(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
PY
    (cd "$SCRIPT_DIR" && sha256sum -c SHA256SUMS)
    awk -F '\t' 'NR==1 {exit !($1=="node" && $3=="ip" && $5=="sequence")}' "$ORCH_NODES"
    awk -F '\t' 'NR==1 {exit !($1=="vip_key" && $2=="address")}' "$VIP_POLICY"
    for required in --static-validate preflight execute verify rollback archive; do
        grep -Fq -- "$required" "$SCRIPT_PATH" || die "missing mode: $required"
    done
    if grep -RIEq '(password[[:space:]]*=|token[[:space:]]*=|BEGIN (RSA |OPENSSH )?PRIVATE KEY)' \
        "$SCRIPT_DIR" --exclude=SHA256SUMS; then
        die 'secret-like material detected'
    fi
    printf 'HA_LOCAL_STATIC_VALIDATE_PASS\n'
    printf 'HA_REMOTE_STATUS=NOT_EXECUTED\n'
}

common_identity_gate() {
    [[ $(id -u) -eq 0 ]] || die 'root is required'
    [[ ${HA_REMOTE_LOCK_GRANTED:-} == 1 ]] || die 'remote lock is not asserted'
    [[ ${HA_REMOTE_LOCK_OWNER:-} == NGINX_Q210_251 ]] || die 'remote lock owner mismatch'
    [[ "$(hostname -s)" == "$target_host" ]] || die 'hostname mismatch'
    ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1 |
        grep -Fxq "$target_ip" || die 'target IP mismatch'
    [[ -r /etc/zhct/node.env ]] || die 'missing /etc/zhct/node.env'
    grep -Fxq "ROLE=$target_role" /etc/zhct/node.env || die 'role mismatch'
    grep -Fxq "NODE_KEY=$node" /etc/zhct/node.env || die 'node key mismatch'
    grep -Fxq "NODE_IP=$target_ip" /etc/zhct/node.env || die 'node IP mismatch'
    printf 'HA_LOCK_IDENTITY_GATE=PASS\n'
}

common_gate() {
    common_identity_gate
    assert_pass Q_DEP_Q102
    assert_pass Q_DEP_Q194
    assert_pass Q_DEP_Q200
    printf 'HA_LOCK_IDENTITY_BASE_DEPENDENCIES=PASS\n'
}

task_dependencies() {
    case "$task" in
        q210) ;;
        q250)
            assert_pass Q_DEP_Q210
            assert_pass Q_DEP_Q024
            assert_pass Q_DEP_Q224
            assert_pass Q_DEP_Q231
            assert_pass Q_DEP_Q240
            ;;
        q251)
            assert_pass Q_DEP_Q210
            assert_pass Q_DEP_Q024
            assert_pass Q_DEP_Q224
            assert_pass Q_DEP_Q231
            assert_pass Q_DEP_Q240
            assert_pass Q_DEP_Q250
            ;;
    esac
    printf 'HA_TASK_DEPENDENCIES=PASS\n'
}

secret_file_gate() {
    [[ ${HA_SECRET_SOURCE_CONFIRMATION:-} == "$SECRET_CONFIRMATION" ]] ||
        die 'trusted same-source secret confirmation is absent'
    local name path mode
    for name in "$@"; do
        path="/etc/zhct/secrets/$name"
        [[ -s "$path" ]] || die "secret file missing or empty: $name"
        [[ $(stat -c '%U:%G' "$path") == root:root ]] || die "unsafe secret owner: $name"
        mode=$(stat -c '%a' "$path")
        [[ "$mode" == 600 || "$mode" == 400 ]] || die "unsafe secret mode: $name"
        printf 'SECRET=%s STATUS=PRESENT_NONEMPTY OWNER=root:root MODE=%s SOURCE=CONFIRMED\n' \
            "$name" "$mode"
    done
}

assert_no_vip_keepalived_off() {
    [[ "$(systemctl is-enabled keepalived 2>/dev/null || true)" == disabled ]] ||
        die 'keepalived must remain disabled'
    [[ "$(systemctl is-active keepalived 2>/dev/null || true)" == inactive ]] ||
        die 'keepalived must remain inactive'
    ! ip -o -4 addr show | grep -Eq '10\.2\.203\.(115|116|117)/' ||
        die 'VIP present before Q-251'
    printf 'HA_KEEPALIVED_DISABLED_VIP_ABSENT=PASS\n'
}

print_orchestrator_safe_config() {
    python3 - "$target_ip" <<'PY'
import json, sys
with open("/etc/orchestrator.conf.json", encoding="utf-8") as handle:
    cfg = json.load(handle)
expected_ip = sys.argv[1]
expected_nodes = ["10.2.203.102", "10.2.203.103", "10.2.203.112"]
checks = {
    "BackendDB": cfg.get("BackendDB") == "sqlite",
    "SQLite3DataFile": cfg.get("SQLite3DataFile") == "/var/lib/orchestrator/orchestrator.db",
    "RaftEnabled": cfg.get("RaftEnabled") is True,
    "RaftDataDir": cfg.get("RaftDataDir") == "/var/lib/orchestrator",
    "RaftBind": cfg.get("RaftBind") == expected_ip,
    "DefaultRaftPort": cfg.get("DefaultRaftPort") == 10008,
    "RaftNodes": cfg.get("RaftNodes") == expected_nodes,
}
for key, passed in checks.items():
    print(f"ORCHESTRATOR_SAFE_FIELD={key} RESULT={'PASS' if passed else 'FAIL'}")
if not all(checks.values()):
    raise SystemExit(1)
PY
}

orchestrator_state_metadata() {
    if [[ -d /var/lib/orchestrator ]]; then
        find /var/lib/orchestrator -maxdepth 1 -mindepth 1 \
            -printf 'ORCHESTRATOR_STATE NAME=%f TYPE=%y MODE=%m OWNER=%u:%g SIZE=%s\n' | sort
    else
        printf 'ORCHESTRATOR_STATE_DIR=ABSENT\n'
    fi
}

q210_preflight() {
    secret_file_gate orchestrator-topology.password
    print_orchestrator_safe_config
    orchestrator_state_metadata
    local peer
    for peer in 10.2.203.102 10.2.203.103 10.2.203.112; do ip route get "$peer"; done
    ss -lntp | grep -E ':(3000|10008)[[:space:]]' || true
    printf 'Q210_PREFLIGHT_PASS\n'
}

q210_execute() {
    [[ ${Q210_CLUSTER_CHANGE_APPROVED:-} == PASS ]] ||
        die 'Q210 cluster change approval is absent'
    [[ ${Q210_STATE_ACTION:-} == QUARANTINE_CLONED_SQLITE_AND_RAFT ]] ||
        die 'Q210 cloned-state quarantine approval is absent'
    [[ ${Q210_NODE_SEQUENCE:-} == "$sequence" ]] || die 'Q210 node sequence mismatch'
    local run_backup="$BACKUP_ROOT/$run_id/$node"
    local isolated="/var/lib/orchestrator.pre-$run_id"
    [[ ! -e "$run_backup" && ! -e "$isolated" ]] || die 'backup or isolated state already exists'
    install -d -m 0700 -o root -g root "$run_backup"
    cp -a /etc/orchestrator.conf.json "$run_backup/orchestrator.conf.json"
    systemctl is-enabled orchestrator >"$run_backup/orchestrator.enabled.before" 2>&1 || true
    systemctl is-active orchestrator >"$run_backup/orchestrator.active.before" 2>&1 || true
    systemctl stop orchestrator.service
    if [[ -d /var/lib/orchestrator ]]; then
        cp -a /var/lib/orchestrator "$run_backup/orchestrator-data-copy"
        mv /var/lib/orchestrator "$isolated"
    fi
    install -d -m 0750 -o root -g root /var/lib/orchestrator
    printf 'ISOLATED_PATH=%s\n' "$isolated" >"$run_backup/state.env"
    printf 'NEW_DATA_PATH=/var/lib/orchestrator\n' >>"$run_backup/state.env"
    systemctl enable --now orchestrator.service
    printf 'Q210_CLONED_SQLITE_RAFT_STATE=QUARANTINED\n'
    printf 'Q210_EXECUTE_NODE_PASS\n'
}

q210_verify() {
    systemctl is-enabled orchestrator
    systemctl is-active --quiet orchestrator
    print_orchestrator_safe_config
    ss -lntp | grep -E "$target_ip:(3000|10008)[[:space:]]" ||
        die 'orchestrator listeners are incomplete'
    curl --fail --silent --show-error --max-time 3 "http://$target_ip:3000/api/health"
    printf '\n'
    curl --fail --silent --show-error --max-time 3 "http://$target_ip:3000/api/raft-health"
    printf '\n'
    printf 'Q210_NODE_VERIFY_PASS\n'
    printf 'Q210_CLUSTER_QUORUM_UNIQUE_IDS_PASS=NOT_ASSERTED_BY_SINGLE_NODE\n'
}

q210_rollback() {
    local run_backup="$BACKUP_ROOT/$run_id/$node"
    local isolated="/var/lib/orchestrator.pre-$run_id"
    [[ -f "$run_backup/state.env" && -d "$isolated" ]] || die 'Q210 rollback state is absent'
    systemctl stop orchestrator.service
    if [[ -d /var/lib/orchestrator ]]; then
        mv /var/lib/orchestrator "/var/lib/orchestrator.failed-$run_id"
    fi
    mv "$isolated" /var/lib/orchestrator
    install -m 0600 -o root -g root "$run_backup/orchestrator.conf.json" /etc/orchestrator.conf.json
    if grep -Fxq active "$run_backup/orchestrator.active.before"; then
        systemctl start orchestrator.service
    fi
    printf 'Q210_ROLLBACK_PASS_FAILED_STATE_PRESERVED\n'
}

haproxy_route_gate() {
    [[ -r /etc/haproxy/haproxy.cfg ]] || die 'HAProxy config missing'
    ! grep -Eq '__[A-Z0-9_]+__' /etc/haproxy/haproxy.cfg || die 'unresolved HAProxy placeholder'
    local required
    while IFS= read -r required; do
        [[ -z "$required" ]] && continue
        grep -Fq "$required" /etc/haproxy/haproxy.cfg || die "missing HAProxy route structure: $required"
    done <"$ROUTES"
    /usr/local/haproxy/sbin/haproxy -c -f /etc/haproxy/haproxy.cfg
    /usr/local/nginx/sbin/nginx -t
    printf 'Q250_ROUTE_CONFIG_GATE=PASS\n'
}

q250_preflight() {
    secret_file_gate redis5.password redis7.password
    assert_no_vip_keepalived_off
    haproxy_route_gate
    systemctl is-active --quiet nginx
    systemctl is-active --quiet haproxy
    printf 'Q250_PREFLIGHT_PASS\n'
}

q250_execute() {
    [[ ${Q250_ROUTE_RELOAD_APPROVED:-} == PASS ]] || die 'Q250 route reload approval absent'
    local run_backup="$BACKUP_ROOT/$run_id/$node"
    [[ ! -e "$run_backup" ]] || die 'Q250 backup already exists'
    install -d -m 0700 -o root -g root "$run_backup"
    cp -a /etc/haproxy/haproxy.cfg "$run_backup/haproxy.cfg"
    cp -a /usr/local/nginx/conf "$run_backup/nginx-conf"
    haproxy_route_gate
    systemctl reload haproxy.service
    systemctl reload nginx.service
    assert_no_vip_keepalived_off
    printf 'Q250_EXECUTE_NODE_PASS\n'
}

q250_verify() {
    haproxy_route_gate
    systemctl is-active --quiet nginx
    systemctl is-active --quiet haproxy
    assert_no_vip_keepalived_off
    ss -lntp | grep -E ':(8081|1883|8083|3306|3308|6379|6387)[[:space:]]' ||
        die 'Q250 expected listeners are absent'
    printf 'Q250_NODE_VERIFY_PASS\n'
    printf 'Q250_GLOBAL_ROUTE_PASS=NOT_ASSERTED_BY_SINGLE_NODE\n'
}

q250_rollback() {
    local run_backup="$BACKUP_ROOT/$run_id/$node"
    [[ -f "$run_backup/haproxy.cfg" && -d "$run_backup/nginx-conf" ]] ||
        die 'Q250 rollback backup absent'
    install -m 0640 -o root -g root "$run_backup/haproxy.cfg" /etc/haproxy/haproxy.cfg
    cp -a "$run_backup/nginx-conf/." /usr/local/nginx/conf/
    haproxy_route_gate
    systemctl reload haproxy.service
    systemctl reload nginx.service
    printf 'Q250_ROLLBACK_PASS\n'
}

vip_address() { awk -F '\t' -v want="$vip" '$1==want {print $2}' "$VIP_POLICY"; }
vip_health() { awk -F '\t' -v want="$vip" '$1==want {print $4}' "$VIP_POLICY"; }

q251_config_gate() {
    secret_file_gate vrrp.auth redis5.password redis7.password
    [[ -r /etc/keepalived/keepalived.conf ]] || die 'keepalived config missing'
    ! grep -Eq '__[A-Z0-9_]+__|@@[A-Z0-9_]+@@' /etc/keepalived/keepalived.conf ||
        die 'unresolved keepalived placeholder'
    grep -Eq "^[[:space:]]*router_id[[:space:]]+SANQUAN_EDGE_0[12]$" \
        /etc/keepalived/keepalived.conf || die 'router_id is missing'
    [[ -x "$(vip_health)" ]] || die 'VIP health script missing'
    "$(vip_health)"
    printf 'Q251_VIP_PREFLIGHT_KEY=%s ADDRESS=%s HEALTH=PASS\n' "$vip" "$(vip_address)"
}

q251_preflight() {
    [[ ${Q251_MAINTENANCE_WINDOW:-} == APPROVED ]] || die 'Q251 maintenance window absent'
    [[ ${Q251_VIP_ENABLE_APPROVED:-} == "$vip" ]] || die 'VIP-specific approval mismatch'
    q251_config_gate
    local addr
    addr=$(vip_address)
    if ip -o -4 addr show | grep -Fq "$addr/"; then
        printf 'Q251_SELECTED_VIP_LOCAL_STATE=PRESENT\n'
    else
        printf 'Q251_SELECTED_VIP_LOCAL_STATE=ABSENT\n'
    fi
    printf 'Q251_PREFLIGHT_NODE_PASS_GLOBAL_FREE_ADDRESS_CHECK_REQUIRED\n'
}

q251_execute() {
    local run_backup="$BACKUP_ROOT/$run_id/$node"
    local base=/etc/zhct/q251-keepalived-base.conf
    local state=/etc/zhct/q251-enabled-vips
    local temp enabled
    [[ ! -e "$run_backup" ]] || die 'Q251 backup already exists'
    install -d -m 0700 -o root -g root "$run_backup"
    cp -a /etc/keepalived/keepalived.conf "$run_backup/keepalived.conf"
    systemctl is-enabled keepalived >"$run_backup/keepalived.enabled.before" 2>&1 || true
    systemctl is-active keepalived >"$run_backup/keepalived.active.before" 2>&1 || true
    [[ ! -e "$state" ]] || cp -a "$state" "$run_backup/enabled-vips.before"
    if [[ ! -e "$base" ]]; then
        for required_instance in FRONT_VIP MQTT_VIP DB_PROXY_VIP; do
            grep -Eq "^[[:space:]]*vrrp_instance[[:space:]]+$required_instance[[:space:]]*\\{" \
                /etc/keepalived/keepalived.conf ||
                die "full keepalived baseline is missing $required_instance"
        done
        install -m 0600 -o root -g root /etc/keepalived/keepalived.conf "$base"
    fi
    enabled=
    [[ ! -f "$state" ]] || enabled=$(paste -sd, "$state")
    case ",$enabled," in *",$vip,"*) ;; *) enabled=${enabled:+$enabled,}$vip ;; esac
    temp=$(mktemp)
    python3 "$SCRIPT_DIR/q251-render-keepalived.py" \
        --input "$base" --output "$temp" --enabled "$enabled"
    /usr/local/sbin/keepalived --config-test --use-file="$temp"
    install -m 0600 -o root -g root "$temp" /etc/keepalived/keepalived.conf
    printf '%s\n' "$enabled" | tr ',' '\n' >"$state"
    chmod 0600 "$state"
    chown root:root "$state"
    systemctl enable keepalived.service
    if systemctl is-active --quiet keepalived.service; then
        systemctl restart keepalived.service
    else
        systemctl start keepalived.service
    fi
    printf 'Q251_EXECUTE_NODE_VIP=%s ENABLED_SET=%s\n' "$vip" "$enabled"
}

q251_verify() {
    q251_config_gate
    systemctl is-enabled keepalived
    systemctl is-active --quiet keepalived
    local state=/etc/zhct/q251-enabled-vips addr listed
    [[ -f "$state" ]] || die 'Q251 enabled VIP state absent'
    grep -Fxq "$vip" "$state" || die 'selected VIP absent from approved state'
    while IFS=$'\t' read -r listed addr _ _; do
        [[ "$listed" == vip_key ]] && continue
        if ip -o -4 addr show | grep -Fq "$addr/"; then
            grep -Fxq "$listed" "$state" || die "unapproved VIP is locally present: $addr"
            printf 'Q251_VIP_LOCAL_OWNER KEY=%s ADDRESS=%s PRESENT=yes\n' "$listed" "$addr"
        else
            printf 'Q251_VIP_LOCAL_OWNER KEY=%s ADDRESS=%s PRESENT=no\n' "$listed" "$addr"
        fi
    done <"$VIP_POLICY"
    printf 'Q251_NODE_VERIFY_PASS\n'
    printf 'Q251_TWO_NODE_UNIQUE_OWNER_AND_FAILOVER_PASS=NOT_ASSERTED_BY_SINGLE_NODE\n'
}

q251_rollback() {
    local run_backup="$BACKUP_ROOT/$run_id/$node"
    local state=/etc/zhct/q251-enabled-vips
    [[ -f "$run_backup/keepalived.conf" ]] || die 'Q251 rollback backup absent'
    systemctl stop keepalived.service
    install -m 0600 -o root -g root "$run_backup/keepalived.conf" /etc/keepalived/keepalived.conf
    if [[ -f "$run_backup/enabled-vips.before" ]]; then
        install -m 0600 -o root -g root "$run_backup/enabled-vips.before" "$state"
    else
        mv "$state" "$run_backup/enabled-vips.after" 2>/dev/null || true
    fi
    if grep -Fxq enabled "$run_backup/keepalived.enabled.before"; then systemctl enable keepalived; else systemctl disable keepalived; fi
    if grep -Fxq active "$run_backup/keepalived.active.before"; then systemctl start keepalived; fi
    printf 'Q251_ROLLBACK_PASS\n'
}

preflight() {
    for c in awk bash cp curl date find grep hostname install ip mv paste python3 sha256sum ss stat systemctl tar tee; do need "$c"; done
    common_gate
    task_dependencies
    case "$task" in q210) q210_preflight ;; q250) q250_preflight ;; q251) q251_preflight ;; esac
    : >"$evidence_dir/$task${vip:+-$vip}.preflight.pass"
}

execute() {
    [[ -f "$evidence_dir/$task${vip:+-$vip}.preflight.pass" ]] || die 'matching preflight marker absent'
    common_gate
    task_dependencies
    case "$task" in q210) q210_execute ;; q250) q250_execute ;; q251) q251_execute ;; esac
    : >"$evidence_dir/$task${vip:+-$vip}.execute.pass"
}

verify() {
    [[ -f "$evidence_dir/$task${vip:+-$vip}.execute.pass" ]] || die 'matching execute marker absent'
    common_gate
    task_dependencies
    case "$task" in q210) q210_verify ;; q250) q250_verify ;; q251) q251_verify ;; esac
    : >"$evidence_dir/$task${vip:+-$vip}.verify.pass"
}

rollback() {
    common_identity_gate
    case "$task" in q210) q210_rollback ;; q250) q250_rollback ;; q251) q251_rollback ;; esac
}

archive() {
    common_identity_gate
    local archive="$evidence_root/${run_id}-${node}${vip:+-$vip}-evidence.tar.gz"
    tar -C "$evidence_root" -czf "$archive" "$run_id/$node"
    sha256sum "$archive"
    printf 'HA_ARCHIVE=%s\n' "$archive"
}

case "$mode" in
    --static-validate) static_validate ;;
    preflight) preflight ;;
    execute) execute ;;
    verify) verify ;;
    rollback) rollback ;;
    archive) archive ;;
esac
