#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077

readonly LABEL=Q160_REDIS_ROLE_READONLY
readonly VIP=10.2.203.117
readonly ROLE_ROOT=/opt/zhct-deploy/roles/redis
readonly Q131_EVIDENCE_SHA256=c4a0ef639ece14dfbe383465a9f3b5bbd4ecbb8e5e66d129830178b43adf9283
readonly Q132_STATE=FAIL_OWNER_REPAIR_PENDING_READONLY_DIAGNOSIS
readonly SCRIPT_PATH="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)/$(basename -- "${BASH_SOURCE[0]}")"
AUDIT_ROOT=/var/backups/zhct-q160/redis

die() {
    printf 'Q160_REDIS_ERROR=%s\n' "$*" >&2
    return 1
}

finish() {
    local rc=$?
    trap - EXIT
    printf 'Q160_REDIS_ROLLBACK_STATE=NO_SYSTEM_CHANGE_TO_ROLL_BACK\n'
    printf 'Q160_REDIS_HA=NOT_ASSERTED\n'
    printf 'Q160_GLOBAL_PASS=NOT_ASSERTED_BY_REDIS_ROLE\n'
    printf 'END|%s|RC=%s|AUDIT_LOG=%s\n' "$LABEL" "$rc" "${AUDIT_LOG:-NOT_CREATED}"
    exit "$rc"
}
trap finish EXIT

static_validate() {
    bash -n "$SCRIPT_PATH"
    grep -Fq "$Q131_EVIDENCE_SHA256" "$SCRIPT_PATH"
    grep -Fq "$Q132_STATE" "$SCRIPT_PATH"
    if grep -En 'systemctl[[:space:]]+(enable|disable|start|stop|restart)|redis-cli[^\n]*(SET|DEL|FLUSH|REPLICAOF|SLAVEOF|FAILOVER)|ip[[:space:]]+addr[[:space:]]+(add|del)' "$SCRIPT_PATH" |
        grep -v 'grep -En'; then
        die 'forbidden system mutation command found'
    fi
    if grep -En 'sha256sum[^\n]*(redis5\.password|redis7\.password)|cat[[:space:]]+[^\n]*(redis5\.password|redis7\.password)' "$SCRIPT_PATH" |
        grep -v 'grep -En'; then
        die 'forbidden secret output or digest command found'
    fi
    printf '%s\n' \
        'Q160_REDIS_LOCAL_STATIC_VALIDATE=PASS' \
        'REMOTE_SYSTEM_MUTATION=ZERO_REQUIRED' \
        'SECRET_VALUES=NOT_OUTPUT_NOT_HASHED' \
        'Q132_REPAIR_ACTIVATION=PROHIBITED'
}

printf 'BEGIN|%s|%s\n' "$LABEL" "$(date '+%Y-%m-%dT%H:%M:%S%z')"
if [[ "${1:-}" == --static-validate ]]; then
    static_validate
    exit 0
fi
[[ $# -eq 0 ]] || die 'no arguments are accepted for remote readonly run'
[[ $(id -u) -eq 0 ]] || die 'root is required'

actual_host=$(hostname -s)
case "$actual_host" in
    sqjcredis01)
        node=redis01
        target_ip=10.2.203.112
        expected_redis_enabled=enabled
        expected_redis_active=active
        expected_secret_state=PRESENT
        expected_placeholder=ABSENT
        node_judgment=PASS_Q131_STATE_PRESERVED
        ;;
    sqjcredis02)
        node=redis02
        target_ip=10.2.203.113
        expected_redis_enabled=disabled
        expected_redis_active=inactive
        expected_secret_state=ABSENT
        expected_placeholder=PRESENT
        node_judgment=PASS_Q132_BLOCKED_STATE_OBSERVED
        ;;
    *) die "unsupported hostname: $actual_host" ;;
esac
ip -o -4 addr show scope global | awk '{split($4,a,"/"); print a[1]}' | grep -Fxq "$target_ip" ||
    die 'target IP mismatch'
printf 'Q160_REDIS_TARGET_IDENTITY=PASS|node=%s|host=%s|ip=%s\n' "$node" "$actual_host" "$target_ip"

RUN_ID="q160-redis-${node}-$(date -u +%Y%m%dT%H%M%SZ)"
AUDIT_DIR="${AUDIT_ROOT}/${RUN_ID}"
AUDIT_LOG="${AUDIT_DIR}/operator.log"
install -d -o root -g root -m 0700 "$AUDIT_DIR"
exec > >(tee -a "$AUDIT_LOG") 2>&1
printf 'AUDIT_BEGIN|%s|%s\n' "$LABEL" "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'Q160_RUN_ID=%s\nQ160_NODE=%s\nQ160_TARGET_HOST=%s\nQ160_TARGET_IP=%s\n' \
    "$RUN_ID" "$node" "$actual_host" "$target_ip"
printf 'Q160_DEP_Q143=PASS_CONTROLLER_ASSERTED_20260805T1745\n'
printf 'Q160_Q131_EVIDENCE_SHA256=%s\n' "$Q131_EVIDENCE_SHA256"
printf 'Q160_Q132_CURRENT_STATE=%s\n' "$Q132_STATE"

printf '%s\n' '=== UNIQUE IDENTITY ==='
[[ -r /etc/zhct/node.env ]] || die 'node.env is missing'
grep -Fxq 'ROLE=redis' /etc/zhct/node.env || die 'node 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'
grep -Fxq "NODE_NAME=$actual_host" /etc/zhct/node.env || die 'node name mismatch'
grep -E '^(ROLE|NODE_KEY|NODE_IP|NODE_NAME|APPLIED_AT)=' /etc/zhct/node.env
[[ -f /etc/machine-id && ! -L /etc/machine-id ]]
stat -c 'MACHINE_ID|%F|%A|%a|%U:%G|%s|%n' /etc/machine-id
[[ $(wc -c </etc/machine-id) -eq 33 ]] || die 'machine-id is not initialized'
private_key_count=$(find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*_key' | wc -l)
public_key_count=$(find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*_key.pub' | wc -l)
printf 'SSH_HOST_KEY_METADATA|private=%s|public=%s|content=NOT_READ_NOT_HASHED\n' "$private_key_count" "$public_key_count"
[[ "$private_key_count" -ge 3 && "$public_key_count" -ge 3 ]] || die 'SSH host keys incomplete'
printf 'Q160_REDIS_UNIQUE_IDENTITY=PASS\n'

printf '%s\n' '=== LOCKED TEMPLATE SOURCE ==='
while IFS='|' read -r expected relative; do
    path="${ROLE_ROOT}/${relative}"
    [[ -f "$path" && ! -L "$path" ]] || die "missing role asset: $relative"
    actual=$(sha256sum "$path" | awk '{print $1}')
    printf 'ROLE_ASSET|%s|expected=%s|actual=%s\n' "$relative" "$expected" "$actual"
    [[ "$actual" == "$expected" ]] || die "role asset mismatch: $relative"
done <<ASSETS
5c7ab26d6a10bc4d0fc362c77c918dde75b2599294e8c374c199b9d08c65bc97|hooks.sh
a9195d2d8347ba1cde725ff3b9ca47b8e839a17972dfae1dceaadd0e475c03ea|role.env
3a5790a52193017e2fdef76fc7c21893b21f14e51927d62314d48b079eba79b5|manifests/nodes.tsv
84d9f92fafd5acd250a2aba1e1a2f4084fb5066814f564b79c072a6afec767e1|manifests/ports.tsv
6eeb07767026b53b461a5d977ce8d0bc4bcb246d1f751e126fc752aa38503bde|manifests/secrets.required.tsv
9bf844d68f6616355ef269cc6d901ccb8f822f4fc30d5bec72d9ff1c10c5eb0b|manifests/systemd-units.tsv
b769b7f766e2eb62e5d24870c0162f6906ddb68daa72c6ae6eb9b9d489c6e6d5|templates/systemd/redis5.service
185bffed16998127dd508ac39c7bc9f60514b7ff90e7ff689a608de607544cd4|templates/systemd/redis7.service
f2b1feb9e87e058e17e3f7a36e405f83ad921484ec655fd04883bfe26d3fc113|templates/systemd/sentinel5.service
d503b2cccdd8102eca7445c98cc6b22d9d3f4f0503d158000e28b10041769368|templates/systemd/sentinel7.service
ASSETS
if [[ "$node" == redis01 ]]; then
    node_assets=$(cat <<'ASSETS'
7d6622ade8a2b42a4152911c71f4fd540114e00f3d49ab4a31d7cc36f745f5c0|templates/nodes/redis01/orchestrator.conf.json
7c85b7cc8fb97b64117c070d0bf61fd7310fd304aa1c25996068904920f27d22|templates/nodes/redis01/redis5.conf
f36db18a63c8d3d10ccfe785cc7227b55e6ea67b56e6b3f8301e6b69081e90cc|templates/nodes/redis01/redis7.conf
3679608812afcc6c3e0044bfe30965c18b129c51de287ddbdd9153dc912aaec1|templates/nodes/redis01/sentinel5.conf
551d97e393a8e947a36cdca33e62b57ee441f1e92e0cedda04be6e5150f1bfc3|templates/nodes/redis01/sentinel7.conf
ASSETS
)
else
    node_assets=$(cat <<'ASSETS'
2b2ce57e346d901529760b4a265c73839384f7a1c0692113cea312cd8012b121|templates/nodes/redis02/orchestrator.conf.json
c79440cd6d43b5205902ec1f2e053e26a767a0b2e0550d112e25ca726c023127|templates/nodes/redis02/redis5.conf
e5ce8c72650df725b97cb2be4b2a1a3220896276bd9e3705d295c71ce4da7b38|templates/nodes/redis02/redis7.conf
447198c2b42f320410190bfe845dc7dfefc43d1fe89e5eed53aa5d5e9ff8bea2|templates/nodes/redis02/sentinel5.conf
0163851f139fdd6bac43f4ba725365ea091b6edc94a58ffb39801f28112c0868|templates/nodes/redis02/sentinel7.conf
ASSETS
)
fi
while IFS='|' read -r expected relative; do
    path="${ROLE_ROOT}/${relative}"
    [[ -f "$path" && ! -L "$path" ]] || die "missing node template: $relative"
    actual=$(sha256sum "$path" | awk '{print $1}')
    printf 'NODE_TEMPLATE|%s|expected=%s|actual=%s\n' "$relative" "$expected" "$actual"
    [[ "$actual" == "$expected" ]] || die "node template mismatch: $relative"
done <<<"$node_assets"
printf 'Q160_REDIS_TEMPLATE_SOURCE=PASS_LOCKED_ROLE_ASSETS\n'

printf '%s\n' '=== BINARIES AND PACKAGES ==='
/opt/redis/5.0.14/bin/redis-server --version
/opt/redis/7.2.14/bin/redis-server --version
rpm -q jq oniguruma percona-orchestrator percona-orchestrator-cli percona-orchestrator-client

printf '%s\n' '=== CONFIG METADATA AND SAFE FIELDS ==='
for config in /etc/redis/redis5.conf /etc/redis/redis7.conf /var/lib/redis-sentinel5/sentinel.conf /var/lib/redis-sentinel7/sentinel.conf /etc/orchestrator.conf.json; do
    if [[ -e "$config" || -L "$config" ]]; then
        stat -c 'CONFIG|PRESENT|%F|%A|%a|%U:%G|%s|%n' "$config"
    else
        printf 'CONFIG|ABSENT|%s\n' "$config"
    fi
done
for config in /etc/redis/redis5.conf /etc/redis/redis7.conf; do
    [[ -f "$config" && ! -L "$config" ]] || die "missing Redis config: $config"
    awk '$1 ~ /^(bind|port|protected-mode|daemonize|dir|dbfilename|appendonly|appendfilename|appenddirname|replicaof|replica-read-only|maxmemory-policy)$/ {print "REDIS_SAFE_FIELD|" FILENAME "|" $0}' "$config"
done
placeholder_state=ABSENT
if grep -Eq '__[A-Z0-9_]+__' /etc/redis/redis5.conf /etc/redis/redis7.conf; then placeholder_state=PRESENT; fi
printf 'REDIS_CONFIG_PLACEHOLDER=%s\n' "$placeholder_state"
[[ "$placeholder_state" == "$expected_placeholder" ]] || die 'Redis config placeholder state mismatch'
for secret in /etc/zhct/secrets/redis5.password /etc/zhct/secrets/redis7.password; do
    if [[ -f "$secret" && ! -L "$secret" ]]; then
        stat -c 'SECRET_METADATA|PRESENT|%F|%A|%a|%U:%G|%s|%n' "$secret"
        [[ "$expected_secret_state" == PRESENT ]] || die 'secret unexpectedly present on blocked Redis02'
        [[ $(stat -c '%U:%G|%a' "$secret") == 'root:root|600' ]]
    else
        printf 'SECRET_METADATA|ABSENT|%s\n' "$secret"
        [[ "$expected_secret_state" == ABSENT ]] || die 'required Redis01 secret absent'
    fi
done
printf 'SECRET_VALUES=NOT_OUTPUT_NOT_HASHED\n'
if [[ -f /etc/orchestrator.conf.json && ! -L /etc/orchestrator.conf.json ]]; then
    python3 - "$target_ip" <<'PY'
import json, sys
with open('/etc/orchestrator.conf.json', encoding='utf-8') as handle:
    config = json.load(handle)
for key in ('ListenAddress','BackendDB','SQLite3DataFile','RaftEnabled','RaftDataDir','RaftBind','DefaultRaftPort','RaftNodes'):
    print(f'ORCHESTRATOR_SAFE_FIELD|{key}={config.get(key)!r}')
PY
fi

printf '%s\n' '=== UNIT PROCESS LISTENER POLICY ==='
for unit in redis5.service redis7.service; do
    enabled=$(systemctl is-enabled "$unit" 2>/dev/null || true)
    active=$(systemctl is-active "$unit" 2>/dev/null || true)
    printf 'UNIT|%s|enabled=%s|active=%s\n' "$unit" "$enabled" "$active"
    [[ "$enabled" == "$expected_redis_enabled" && "$active" == "$expected_redis_active" ]] || die "$unit state mismatch"
done
for unit in sentinel5.service sentinel7.service orchestrator.service; do
    enabled=$(systemctl is-enabled "$unit" 2>/dev/null || true)
    active=$(systemctl is-active "$unit" 2>/dev/null || true)
    printf 'UNIT|%s|enabled=%s|active=%s\n' "$unit" "$enabled" "$active"
    [[ "$enabled" == disabled && "$active" == inactive ]] || die "$unit must remain disabled/inactive"
done
keepalived_active=$(systemctl is-active keepalived.service 2>/dev/null || true)
printf 'UNIT|keepalived.service|active=%s\n' "$keepalived_active"
[[ "$keepalived_active" != active ]] || die 'keepalived must not be active before Q-251'
ps -eo pid=,user=,comm=,args= | awk '$3 ~ /^(redis-server|redis-sentinel|orchestrator)$/ {print "ROLE_PROCESS|" $0}'
listeners=$(ss -lntupH | awk '$5 ~ /:(6379|6387|26379|26387|3000|10008)$/ {print}')
if [[ -n "$listeners" ]]; then printf '%s\n' "$listeners" | sed 's/^/TARGET_LISTENER|/'; else printf 'TARGET_LISTENERS=NONE\n'; fi
if [[ "$node" == redis01 ]]; then
    ss -lntH | awk '{print $4}' | grep -Fxq "$target_ip:6379" || die 'Redis5 listener absent on Redis01'
    ss -lntH | awk '{print $4}' | grep -Fxq "$target_ip:6387" || die 'Redis7 listener absent on Redis01'
    for port in 26379 26387 3000 10008; do
        ! ss -lntH | awk '{print $4}' | grep -Eq ":${port}$" || die "forbidden listener present: $port"
    done
    IFS= read -r redis5_auth </etc/zhct/secrets/redis5.password
    IFS= read -r redis7_auth </etc/zhct/secrets/redis7.password
    printf 'REDIS5_AUTH_PING=%s\n' "$(REDISCLI_AUTH="$redis5_auth" /opt/redis/5.0.14/bin/redis-cli --no-auth-warning -h "$target_ip" -p 6379 PING)"
    printf 'REDIS7_AUTH_PING=%s\n' "$(REDISCLI_AUTH="$redis7_auth" /opt/redis/7.2.14/bin/redis-cli --no-auth-warning -h "$target_ip" -p 6387 PING)"
    REDISCLI_AUTH="$redis5_auth" /opt/redis/5.0.14/bin/redis-cli --no-auth-warning -h "$target_ip" -p 6379 INFO replication | grep -E '^(role|connected_slaves):'
    REDISCLI_AUTH="$redis7_auth" /opt/redis/7.2.14/bin/redis-cli --no-auth-warning -h "$target_ip" -p 6387 INFO replication | grep -E '^(role|connected_slaves):'
    unset redis5_auth redis7_auth
else
    [[ -z "$listeners" ]] || die 'Redis02 target listener exists while Q132 remains blocked'
    printf 'REDIS_AUTH_PROBE=NOT_RUN_Q132_BLOCKED_SERVICES_INACTIVE\n'
fi
! ip -o -4 addr show | grep -Fq "$VIP/" || die 'VIP present before Q-251'
printf 'Q160_REDIS_VIP_ABSENT=PASS|vip=%s\n' "$VIP"
printf 'Q160_REDIS_NODE_READONLY=%s\n' "$node_judgment"
if [[ "$node" == redis01 ]]; then
    printf 'Q160_REDIS_ROLE_JUDGMENT=BLOCKED_PENDING_REDIS02_Q132\n'
else
    printf 'Q160_REDIS_ROLE_JUDGMENT=BLOCKED_BY_Q132_NOT_PASS\n'
fi
printf 'Q160_REDIS_NODE_READONLY_PASS\n'
