#!/usr/bin/env bash

set -Eeuo pipefail

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
export ZHCT_STATIC_VALIDATE=1
roles=(nginx app mysql redis emqx)
actions=(install verify seal post-clone activate rollback)
errors=0
rollback_fixture=$(mktemp -d)
trap 'rm -rf "$rollback_fixture"' EXIT
mkdir -p "$rollback_fixture/test-backup"

fail() {
    printf 'FAIL: %s\n' "$*" >&2
    errors=$((errors + 1))
}

for script in $(find "$ROOT" -type f -name '*.sh' -print | sort); do
    bash -n "$script" || fail "bash syntax: $script"
done

if "$ROOT/common/bin/role-action.sh" install --role unknown --bundle-root "$ROOT" --dry-run >/dev/null 2>&1; then
    fail 'unknown role was accepted'
fi
pending_fixture="$rollback_fixture/pending.tsv"
printf 'artifact\tsha256\tstatus\tsource\nfixture.tar.gz\tPENDING\tPENDING\tvalidator\n' >"$pending_fixture"
if (
    export DRY_RUN=0 BUNDLE_ROOT="$ROOT"
    # shellcheck source=common/lib/runtime.sh
    source "$ROOT/common/lib/runtime.sh"
    verify_locked_assets "$pending_fixture"
) >/dev/null 2>&1; then
    fail 'live install digest gate accepted PENDING asset'
fi
rpm_fixture="$rollback_fixture/rpm-fixture"
mkdir -p "$rpm_fixture/rpms"
printf 'fixture-rpm-a\n' >"$rpm_fixture/rpms/a.rpm"
printf 'fixture-rpm-b\n' >"$rpm_fixture/rpms/b.rpm"
(
    cd "$rpm_fixture"
    sha256sum rpms/a.rpm rpms/b.rpm >rpms.sha256
)
if ! (
    export DRY_RUN=0 BUNDLE_ROOT="$ROOT"
    # shellcheck source=common/lib/runtime.sh
    source "$ROOT/common/lib/runtime.sh"
    verify_rpm_closure_manifest "$rpm_fixture/rpms.sha256"
) >/dev/null 2>&1; then
    fail 'valid RPM closure manifest was rejected'
fi
rm -f "$rpm_fixture/rpms/b.rpm"
if (
    export DRY_RUN=0 BUNDLE_ROOT="$ROOT"
    # shellcheck source=common/lib/runtime.sh
    source "$ROOT/common/lib/runtime.sh"
    verify_rpm_closure_manifest "$rpm_fixture/rpms.sha256"
) >/dev/null 2>&1; then
    fail 'RPM closure manifest accepted a missing RPM'
fi
printf 'fixture-rpm-b\n' >"$rpm_fixture/rpms/b.rpm"
printf 'fixture-rpm-extra\n' >"$rpm_fixture/rpms/extra.rpm"
if (
    export DRY_RUN=0 BUNDLE_ROOT="$ROOT"
    # shellcheck source=common/lib/runtime.sh
    source "$ROOT/common/lib/runtime.sh"
    verify_rpm_closure_manifest "$rpm_fixture/rpms.sha256"
) >/dev/null 2>&1; then
    fail 'RPM closure manifest accepted an extra RPM'
fi
grep -Fq 'clone identity already belongs to' "$ROOT/common/lib/runtime.sh" || fail 'post-clone identity regeneration is not idempotent'
grep -Fq 'rm -f /etc/ssh/ssh_host_ed25519_key' "$ROOT/common/lib/runtime.sh" || fail 'post-clone does not reset inherited SSH host keys'
seal_stop_line=$(awk '/^[[:space:]]+seal\)/ {inside=1} inside && /stop_disable_units/ {print NR; exit}' "$ROOT/common/bin/role-action.sh")
seal_hook_line=$(awk '/^[[:space:]]+seal\)/ {inside=1} inside && /role_seal/ {print NR; exit}' "$ROOT/common/bin/role-action.sh")
[[ -n "$seal_stop_line" && -n "$seal_hook_line" && "$seal_stop_line" -lt "$seal_hook_line" ]] || fail 'seal must stop units before removing runtime state'
grep -Fq 'verify_template_units_inactive()' "$ROOT/common/lib/runtime.sh" || fail 'template inactive unit verifier is missing'
grep -Fq 'find /var/lib/redis-sentinel5 /var/lib/redis-sentinel7 -mindepth 1 -delete' "$ROOT/roles/nginx/hooks.sh" || fail 'Nginx seal does not remove hidden Sentinel state'
grep -Fq 'rm -f /etc/keepalived/keepalived.conf /etc/haproxy/haproxy.cfg /etc/orchestrator.conf.json' "$ROOT/roles/nginx/hooks.sh" || fail 'Nginx seal does not remove materialized secrets'
grep -Fq 'find /var/lib/emqx /var/log/emqx -mindepth 1 -delete' "$ROOT/roles/emqx/hooks.sh" || fail 'EMQX seal does not remove hidden runtime state'
grep -Fq 'rm -f /etc/emqx/emqx.conf' "$ROOT/roles/emqx/hooks.sh" || fail 'EMQX seal does not remove the materialized cluster cookie'
grep -Fq 'find "$path" -mindepth 1 ! -type d' "$ROOT/roles/mysql/hooks.sh" || fail 'MySQL seal does not reject all persistent state'
grep -Fq 'rm -f /etc/redis/redis5.conf /etc/redis/redis7.conf /etc/orchestrator.conf.json' "$ROOT/roles/redis/hooks.sh" || fail 'Redis seal does not remove materialized secrets'
mysql_guard_line=$(awk '/^role_seal\(\)/ {inside=1} inside && /refuse to seal a template containing MySQL state/ {print NR; exit}' "$ROOT/roles/mysql/hooks.sh")
mysql_clean_line=$(awk '/^role_seal\(\)/ {inside=1} inside && /rm -f \/etc\/zhct\/node.env/ {print NR; exit}' "$ROOT/roles/mysql/hooks.sh")
[[ -n "$mysql_guard_line" && -n "$mysql_clean_line" && "$mysql_guard_line" -lt "$mysql_clean_line" ]] || fail 'MySQL seal must reject persistent state before identity cleanup'
redis_guard_line=$(awk '/^role_seal\(\)/ {inside=1} inside && /refuse to seal template with runtime state/ {print NR; exit}' "$ROOT/roles/redis/hooks.sh")
redis_clean_line=$(awk '/^role_seal\(\)/ {inside=1} inside && /rm -f \/etc\/zhct\/node.env/ {print NR; exit}' "$ROOT/roles/redis/hooks.sh")
[[ -n "$redis_guard_line" && -n "$redis_clean_line" && "$redis_guard_line" -lt "$redis_clean_line" ]] || fail 'Redis seal must reject persistent state before identity cleanup'

for role in "${roles[@]}"; do
    role_root="$ROOT/roles/$role"
    for file in role.env hooks.sh manifests/packages.tsv manifests/packages.sha256 manifests/asset-digests.tsv manifests/systemd-units.tsv manifests/ports.tsv manifests/nodes.tsv manifests/secrets.required.tsv manifests/repo-snapshot.txt manifests/binary-runtime.tsv; do
        [[ -f "$role_root/$file" ]] || fail "missing $role/$file"
    done

    [[ $(awk -F'\t' 'NR==1 {print NF}' "$role_root/manifests/packages.tsv") -eq 13 ]] || fail "$role packages.tsv header must have 13 fields"
    awk -F'\t' 'NR>1 && NF != 13 {exit 1}' "$role_root/manifests/packages.tsv" || fail "$role packages.tsv row field count"
    awk -F'\t' -v role="$role" 'NR>1 && NF && $1 != role {exit 1}' "$role_root/manifests/packages.tsv" || fail "$role packages.tsv contains another role"
    actual_components=$(awk -F'\t' 'NR>1 && NF {print $2}' "$role_root/manifests/packages.tsv" | sort -u | paste -sd, -)
    case "$role" in
        nginx) expected_components='haproxy,keepalived,lnmp-framework,nginx,openeuler-runtime,percona-orchestrator,percona-orchestrator-cli,percona-orchestrator-client,redis-sentinel5-runtime,redis-sentinel7-runtime' ;;
        app) expected_components='cronie,libmosquitto,lnmp-framework,mysql56-client,nginx,openeuler-runtime,php,php-mosquitto,phpredis,redis-cli,supervisor' ;;
        mysql) expected_components='lnmp-framework,mysql,openeuler-runtime' ;;
        redis) expected_components='openeuler-runtime,percona-orchestrator,percona-orchestrator-cli,percona-orchestrator-client,redis' ;;
        emqx) expected_components='emqx-oss,mosquitto-clients,openeuler-runtime' ;;
    esac
    [[ "$actual_components" == "$expected_components" ]] || fail "$role package role boundary changed: $actual_components"
    [[ $(awk -F'\t' 'NR==1 {print NF}' "$role_root/manifests/asset-digests.tsv") -eq 4 ]] || fail "$role asset-digests.tsv header must have 4 fields"
    awk -F'\t' 'NR>1 && NF != 4 {exit 1}' "$role_root/manifests/asset-digests.tsv" || fail "$role asset-digests.tsv row field count"
    awk -F'\t' 'NR>1 && NF && $3 !~ /^(LOCKED|PENDING|REPO_SNAPSHOT)$/ {exit 1}' "$role_root/manifests/asset-digests.tsv" || fail "$role digest status"
    awk -F'\t' 'NR>1 && NF && $3 == "LOCKED" && $2 !~ /^[0-9a-f]{64}$/ {exit 1}' "$role_root/manifests/asset-digests.tsv" || fail "$role locked SHA256"
    awk 'NF && $1 !~ /^#/ && ($1 !~ /^[0-9a-f]{64}$/ || $2 !~ /^packages\//) {exit 1}' "$role_root/manifests/packages.sha256" || fail "$role packages.sha256 format"
    while IFS=$'\t' read -r artifact sha status source; do
        [[ "$artifact" == artifact || "$status" != LOCKED ]] && continue
        grep -Fq "$sha  packages/$artifact" "$role_root/manifests/packages.sha256" || fail "$role locked asset missing from packages.sha256: $artifact"
    done <"$role_root/manifests/asset-digests.tsv"
    [[ $(awk -F'\t' 'NR==1 {print NF}' "$role_root/manifests/nodes.tsv") -eq 5 ]] || fail "$role nodes.tsv header must have 5 fields"
    awk -F'\t' 'NR>1 && NF != 5 {exit 1}' "$role_root/manifests/nodes.tsv" || fail "$role nodes.tsv row field count"
    [[ $(awk -F'\t' 'NR==1 {print NF}' "$role_root/manifests/systemd-units.tsv") -eq 4 ]] || fail "$role systemd-units.tsv header must have 4 fields"
    awk -F'\t' 'NR>1 && NF != 4 {exit 1} NR>1 && seen[$1]++ {exit 1}' "$role_root/manifests/systemd-units.tsv" || fail "$role systemd unit row or duplicate"
    [[ $(awk -F'\t' 'NR==1 {print NF}' "$role_root/manifests/ports.tsv") -eq 5 ]] || fail "$role ports.tsv header must have 5 fields"
    awk -F'\t' 'NR>1 && NF != 5 {exit 1} NR>1 && seen[$1 FS $2]++ {exit 1}' "$role_root/manifests/ports.tsv" || fail "$role port row or duplicate"
    while IFS=$'\t' read -r node ip hostname profile clone_source; do
        [[ "$node" == node_key ]] && continue
        awk -F'\t' -v node="$node" 'NR>1 && index($3, node "=") == 0 {exit 1}' "$role_root/manifests/systemd-units.tsv" || fail "$role unit policy missing node $node"
    done <"$role_root/manifests/nodes.tsv"
    while IFS= read -r placeholder; do
        [[ -z "$placeholder" ]] && continue
        grep -Fq "$placeholder" "$role_root/hooks.sh" || fail "$role template placeholder has no materialization hook: $placeholder"
    done < <(rg -o --no-filename '__[A-Z0-9_]+__' "$role_root/templates" | sort -u)
    ! rg -n '\blatest\b' "$role_root/manifests" >/dev/null || fail "$role manifest contains latest"
    grep -Fq 'rm -rf /etc/zhct/secrets' "$role_root/hooks.sh" || fail "$role seal does not remove role secrets"
    grep -Fq 'verify_template_units_inactive "$UNITS_MANIFEST"' "$role_root/hooks.sh" || fail "$role verify does not enforce inactive template units"
done

node_inventory="$rollback_fixture/all-nodes.tsv"
awk -F'\t' 'FNR>1 && NF {print $1 "\t" $2 "\t" tolower($3)}' "$ROOT"/roles/*/manifests/nodes.tsv >"$node_inventory"
awk -F'\t' 'seen_node[$1]++ || seen_ip[$2]++ || seen_hostname[$3]++ {exit 1}' "$node_inventory" || fail 'node key, IP or hostname is not globally unique'

for role in "${roles[@]}"; do
    for action in "${actions[@]}"; do
        case "$action" in
            post-clone)
                node=$(awk -F'\t' 'NR==2 {print $1}' "$ROOT/roles/$role/manifests/nodes.tsv")
                ip=$(awk -F'\t' 'NR==2 {print $2}' "$ROOT/roles/$role/manifests/nodes.tsv")
                command=("$ROOT/common/bin/role-action.sh" "$action" --role "$role" --bundle-root "$ROOT" --node "$node" --expected-ip "$ip" --dry-run)
                ;;
            rollback)
                command=(env BACKUP_ROOT="$rollback_fixture" "$ROOT/common/bin/role-action.sh" "$action" --role "$role" --bundle-root "$ROOT" --backup-id test-backup --dry-run)
                ;;
            activate)
                node=$(awk -F'\t' 'NR==2 {print $1}' "$ROOT/roles/$role/manifests/nodes.tsv")
                command=("$ROOT/common/bin/role-action.sh" "$action" --role "$role" --bundle-root "$ROOT" --node "$node" --dry-run)
                ;;
            seal)
                command=("$ROOT/common/bin/role-action.sh" "$action" --role "$role" --bundle-root "$ROOT" --dry-run)
                ;;
            *)
                command=("$ROOT/common/bin/role-action.sh" "$action" --role "$role" --bundle-root "$ROOT" --dry-run)
                ;;
        esac
        "${command[@]}" >/dev/null 2>&1 || fail "$role $action dry-run"
    done
done

for role in "${roles[@]}"; do
    while IFS=$'\t' read -r node ip hostname profile clone_source; do
        [[ "$node" == node_key ]] && continue
        "$ROOT/common/bin/role-action.sh" post-clone --role "$role" --bundle-root "$ROOT" --node "$node" --expected-ip "$ip" --dry-run >/dev/null 2>&1 || fail "$role/$node post-clone dry-run"
        "$ROOT/common/bin/role-action.sh" activate --role "$role" --bundle-root "$ROOT" --node "$node" --dry-run >/dev/null 2>&1 || fail "$role/$node activate dry-run"
    done <"$ROOT/roles/$role/manifests/nodes.tsv"
done

app_root="$ROOT/roles/app"
"$app_root/templates/runtime/singleton-state-check.sh" --node app01 --config "$app_root/templates/nodes/app01/supervisor.conf" --cron "$app_root/templates/nodes/app01/crontab" >/dev/null || fail 'app01 singleton policy'
"$app_root/templates/runtime/singleton-state-check.sh" --node app02 --config "$app_root/templates/nodes/app02/supervisor.conf" --cron "$app_root/templates/nodes/app02/crontab" >/dev/null || fail 'app02 singleton policy'
APP01_CONF="$app_root/templates/nodes/app01/supervisor.conf" APP02_CONF="$app_root/templates/nodes/app02/supervisor.conf" "$app_root/templates/runtime/mqtt-runtime-check.sh" --dry-run >/dev/null || fail 'MQTT static policy'
"$app_root/templates/runtime/redis-runtime-check.sh" --dry-run >/dev/null || fail 'Redis5 DB0 dry-run'
redis_fixture="$rollback_fixture/redis-config"
mkdir -p "$redis_fixture"
printf '%s\n' '[REDIS]' 'HOSTNAME = 10.2.203.117' 'HOSTPORT = 6379' >"$redis_fixture/ai.env"
printf '%s\n' '<?php' '$config['\''redis'\''] = [' "    'host' => '10.2.203.117'," "    'port' => 6379," '];' >"$redis_fixture/store.php"
AI_ENV="$redis_fixture/ai.env" STORE_CONFIG="$redis_fixture/store.php" "$app_root/templates/runtime/redis-runtime-check.sh" --config-only >/dev/null || fail 'implicit Redis DB0 config'
printf '%s\n' 'SELECT = 0' >>"$redis_fixture/ai.env"
AI_ENV="$redis_fixture/ai.env" STORE_CONFIG="$redis_fixture/store.php" "$app_root/templates/runtime/redis-runtime-check.sh" --config-only >/dev/null || fail 'explicit Redis DB0 config'
sed -i.bak 's/SELECT = 0/SELECT = 1/' "$redis_fixture/ai.env"
if AI_ENV="$redis_fixture/ai.env" STORE_CONFIG="$redis_fixture/store.php" "$app_root/templates/runtime/redis-runtime-check.sh" --config-only >/dev/null 2>&1; then fail 'AI Redis nonzero DB was accepted'; fi
"$app_root/templates/runtime/mysql-backup-wrapper.sh" --dry-run >/dev/null || fail 'MySQL backup dry-run'
[[ -x "$app_root/templates/runtime/app-runtime-control.sh" ]] || fail 'App Q-263 runtime control is not executable'
for action in enable disable status; do
    "$app_root/templates/runtime/app-runtime-control.sh" "$action" --node app01 --dry-run >/dev/null || fail "App runtime $action dry-run"
done
awk -F'\t' 'NR>1 && NF != 9 {exit 1}' "$app_root/manifests/app01-file-route-manifest.tsv" || fail 'file route manifest field count'
grep -Eq '^[[:space:]]*listen 8081;' "$app_root/templates/nginx/app.conf" || fail 'App public 8081 listener missing'
grep -Eq '^[[:space:]]*listen 127\.0\.0\.1:18081;' "$app_root/templates/nginx/app.conf" || fail 'AI loopback listener missing'
grep -Fq 'location ^~ /aizhct/' "$app_root/templates/nginx/app.conf" || fail 'AI /aizhct route missing'
grep -Fq 'fastcgi_pass unix:/run/php-fpm73/php-fpm.sock' "$app_root/templates/nginx/app.conf" || fail 'Store PHP7.3 route missing'
grep -Fq 'fastcgi_pass unix:/run/php-fpm74/php-fpm.sock' "$app_root/templates/nginx/app.conf" || fail 'AI PHP7.4 route missing'
! grep -Fq 'location ^~ /api/' "$app_root/templates/nginx/app.conf" || fail 'Store /api must not be captured by AI PHP7.4'
! rg -n '^[[:space:]]*listen[[:space:]]+(80|81|443)([[:space:];]|$)' "$app_root/templates/nginx" >/dev/null || fail 'legacy App 80/81/443 listener found'
grep -Fq 'templates/php/php73.ini' "$app_root/hooks.sh" || fail 'PHP7.3 common config is not staged before verify'
grep -Fq 'templates/php/php74.ini' "$app_root/hooks.sh" || fail 'PHP7.4 common config is not staged before verify'
grep -Fq 'app-runtime-control.sh' "$app_root/hooks.sh" || fail 'App Q-263 runtime control is not installed'
grep -Fq 'APP01_CONF=${APP01_CONF:-/etc/zhct/templates/app/nodes/app01/supervisor.conf}' "$app_root/templates/runtime/mqtt-runtime-check.sh" || fail 'MQTT runtime template path is not sealed path'
grep -Fq 'supervisor.service	disabled,inactive	app01=disabled,inactive;app02=disabled,inactive' "$app_root/manifests/systemd-units.tsv" || fail 'Supervisor must remain disabled until Q-263'
grep -Fq 'crond.service	disabled,inactive	app01=disabled,inactive;app02=disabled,inactive' "$app_root/manifests/systemd-units.tsv" || fail 'crond must remain disabled until Q-263'
grep -Fq 'systemctl enable --now supervisor.service' "$app_root/templates/runtime/app-runtime-control.sh" || fail 'Supervisor explicit Q-263 enable gate missing'
grep -Fq 'systemctl enable --now crond.service' "$app_root/templates/runtime/app-runtime-control.sh" || fail 'crond explicit Q-263 enable gate missing'
grep -Fq 'ServerOptions().realize' "$app_root/templates/runtime/app-runtime-control.sh" || fail 'Supervisor parser validation gate missing'
! grep -Eq 'supervisord[[:space:]]+-t([[:space:]]|$)' "$app_root/templates/runtime/app-runtime-control.sh" || fail 'Supervisor strip-ANSI option is misused as config validation'
release_root="$app_root/templates/release"
[[ $(awk 'NF {count++} END {print count+0}' "$release_root/release-packages.sha256") -eq 3 ]] || fail 'release checksum manifest must contain three packages'
awk 'NF && ($1 !~ /^[0-9a-f]{64}$/ || $2 !~ /^(zhct_132|aizhct_61|store_14)\.tgz$/) {exit 1}' "$release_root/release-packages.sha256" || fail 'release checksum manifest format'
for script in release-common deploy_zhct_sanquan deploy_aizhct_sanquan deploy_ai_store_sanquan; do
    [[ -x "$release_root/scripts/$script.sh" ]] || fail "release script not executable: $script"
done
! rg -n 'https?://|wget|curl' "$release_root/scripts" >/dev/null || fail 'release scripts contain remote download behavior'
grep -Fq 'public/static/upload' "$release_root/scripts/deploy_zhct_sanquan.sh" || fail 'Store upload preservation missing'
grep -Fq 'public/store' "$release_root/scripts/deploy_zhct_sanquan.sh" || fail 'AI Store preservation missing from Store deploy'
grep -Fq 'public/uploads' "$release_root/scripts/deploy_aizhct_sanquan.sh" || fail 'AI uploads preservation missing'
grep -Fq 'public/downloads' "$release_root/scripts/deploy_aizhct_sanquan.sh" || fail 'AI downloads preservation missing'
grep -Fq "BASE_API: '/aizhct/index.php?s=/store'" "$release_root/scripts/deploy_ai_store_sanquan.sh" || fail 'AI Store same-origin API patch missing'
grep -Fq 'deployment failed; restoring previous release' "$release_root/scripts/release-common.sh" || fail 'release rollback path missing'
grep -Fq '/opt/zhct-release/scripts' "$app_root/hooks.sh" || fail 'release scripts are not installed by App template'
grep -Fq '^/aizhct/' "$ROOT/roles/nginx/templates/nginx/file-primary-routes.conf" || fail 'AI App01 write-route prefix missing'
grep -Fq -- '--verbose --help' "$ROOT/roles/mysql/hooks.sh" || fail 'MySQL5.6 config parse check missing'
! grep -Fq -- '--validate-config >/dev/null 2>&1 || true' "$ROOT/roles/mysql/hooks.sh" || fail 'MySQL5.6 validation failure is masked'
for role in nginx redis; do
    grep -Fq '/var/lib/redis-sentinel5/sentinel.conf' "$ROOT/roles/$role/templates/systemd/sentinel5.service" || fail "$role Sentinel5 writable state config missing"
    grep -Fq '/var/lib/redis-sentinel7/sentinel.conf' "$ROOT/roles/$role/templates/systemd/sentinel7.service" || fail "$role Sentinel7 writable state config missing"
    ! rg -n '/etc/redis/sentinel(5|7)\.conf' "$ROOT/roles/$role" >/dev/null || fail "$role uses read-only Sentinel config path"
    while IFS= read -r sentinel; do
        grep -Fq 'protected-mode no' "$sentinel" || fail "$role Sentinel cross-node protected-mode policy missing: $sentinel"
    done < <(find "$ROOT/roles/$role/templates/nodes" -type f -name 'sentinel*.conf' | sort)
    ! rg -n 'Type=notify|--supervised systemd' "$ROOT/roles/$role/templates/systemd" -g 'redis*.service' -g 'sentinel*.service' >/dev/null || fail "$role Redis unit depends on optional sd_notify build"
done

nginx_root="$ROOT/roles/nginx"
for probe in check-front-vip check-mqtt-vip check-db-proxy-vip vip-control provision-edge-secrets; do
    [[ -x "$nginx_root/templates/runtime/$probe.sh" ]] || fail "edge probe not executable: $probe"
done
"$nginx_root/templates/runtime/provision-edge-secrets.sh" --dry-run >/dev/null || fail 'edge secret provisioning dry-run'
emqx_root="$ROOT/roles/emqx"
[[ -x "$emqx_root/templates/runtime/provision-emqx-cookie.sh" ]] || fail 'EMQX cookie provisioner not executable'
"$emqx_root/templates/runtime/provision-emqx-cookie.sh" --dry-run >/dev/null || fail 'EMQX cookie provisioning dry-run'
for node in nginx01 nginx02; do
    keepalived="$nginx_root/templates/nodes/$node/keepalived.conf"
    [[ $(grep -Ec '^[[:space:]]*vrrp_instance[[:space:]]+' "$keepalived") -eq 3 ]] || fail "$node must define three VRRP instances"
    [[ $(awk '/virtual_router_id/ {print $2}' "$keepalived" | sort -n | tr '\n' ' ') == '115 116 117 ' ]] || fail "$node VRIDs must be 115, 116 and 117"
    for instance in FRONT_VIP MQTT_VIP DB_PROXY_VIP; do
        grep -Fq "vrrp_instance $instance" "$keepalived" || fail "$node missing $instance"
    done
    for probe in chk_front_vip chk_mqtt_vip chk_db_proxy_vip; do
        [[ $(grep -Fc "$probe" "$keepalived") -ge 2 ]] || fail "$node does not track $probe"
    done
done
grep -Fq 'location = /__edge_health' "$nginx_root/templates/nginx/front.conf" || fail 'edge health endpoint missing'
! rg -n '^[[:space:]]*listen[[:space:]]+(80|443)([[:space:];]|$)' "$ROOT/roles" >/dev/null || fail 'disabled 80/443 listener found'
for port in 3306 3308 6379 6387 1883 8083; do
    grep -Eq "^[[:space:]]+bind [^[:space:]]+:$port$" "$nginx_root/templates/haproxy/haproxy.cfg" || fail "HAProxy listener missing: $port"
done
grep -Fq 'ExecReload=/bin/kill -USR2 $MAINPID' "$nginx_root/templates/systemd/haproxy.service" || fail 'HAProxy smooth reload signal missing'
grep -Fq -- '--use-file=/etc/keepalived/keepalived.conf' "$nginx_root/templates/systemd/keepalived.service" || fail 'Keepalived explicit config path missing'
! rg -n -- '--config(-file)?=' "$nginx_root/hooks.sh" "$nginx_root/templates/systemd/keepalived.service" >/dev/null || fail 'unsupported Keepalived config option found'
grep -Fq 'nginx01=disabled,inactive;nginx02=disabled,inactive' "$nginx_root/manifests/systemd-units.tsv" || fail 'Keepalived must remain disabled until Q-251'
grep -Fq 'systemctl enable --now keepalived.service' "$nginx_root/templates/runtime/vip-control.sh" || fail 'explicit VIP enable gate missing'

if rg -n --hidden -g '!README.md' -g '!validate-assets.sh' -e 'password\s*[:=]\s*[^_@[:space:]]' -e 'BEGIN (RSA |OPENSSH )?PRIVATE KEY' -e 'guest10[78]' -e 'sqjiucan01' "$ROOT"; then
    fail "possible plaintext credential"
fi

if ((errors > 0)); then
    printf '%s validation error(s)\n' "$errors" >&2
    exit 1
fi

"$ROOT/bundle-sha256.sh" --check >/dev/null || fail "bundle SHA256"

if ((errors > 0)); then
    printf '%s validation error(s)\n' "$errors" >&2
    exit 1
fi

printf 'PASS: %s roles, shell syntax, manifests, dry-run and secret scan\n' "${#roles[@]}"
