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

PROGRAM=$(basename "$0")
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
EVIDENCE_BASE=${EVIDENCE_BASE:-/var/backups/zhct-q210-231/redis}
PHASE= NODE= ACTION= INPUT_FILE= RUN_ID= SOURCE_RUN_ID=

usage() {
    cat <<'USAGE'
Usage:
  redis-ha-q210-231 --static-validate
  redis-ha-q210-231 PHASE NODE preflight|execute|verify|rollback [--input-file PATH] [--run-id ID] [--source-run-id ID]
  redis-ha-q210-231 PHASE NODE archive --source-run-id ID
USAGE
}
timestamp() { date '+%Y-%m-%dT%H:%M:%S%z'; }
log() { printf '%s [%s] %s\n' "$(timestamp)" "$PROGRAM" "$*"; }
die() { log "ERROR: $*"; exit 1; }
valid_id() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$ ]]; }
valid_sha() { [[ "$1" =~ ^[0-9a-f]{64}$ ]]; }
need() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }
sanitize() {
    sed -E 's/([Pp]assword|[Tt]oken|[Cc]ookie|[Ss]ecret)([[:space:]]*[:=][[:space:]]*)[^[:space:]]+/\1\2[REDACTED]/g'
}

static_validate() {
    [[ -f "$SCRIPT_DIR/SHA256SUMS" ]] || die "SHA256SUMS missing"
    (cd "$SCRIPT_DIR" && sha256sum -c SHA256SUMS)
    bash -n "$SCRIPT_DIR/redis-ha-q210-231"
    grep -Fqx $'redis01\t10.2.203.112\tsqjcredis01\t10008' "$SCRIPT_DIR/manifests/orchestrator-raft.tsv"
    grep -Fqx $'redis02\t10.2.203.113\tsqjcredis02\t26379\t26387' "$SCRIPT_DIR/manifests/sentinel-members.tsv"
    grep -Fqx $'q231-db-proxy\tnginx02\tQ200_PASS_SHA,Q024_PASS_SHA,Q230_PASS_SHA\tstage HAProxy Redis route\tmaintenance ID\tstaged only; VIP absent' "$SCRIPT_DIR/manifests/phase-policy.tsv"
    grep -Fq 'bind 10.2.203.117:6379' "$SCRIPT_DIR/templates/haproxy-redis-route.cfg"
    grep -Fq 'role:master' "$SCRIPT_DIR/templates/haproxy-redis-route.cfg"
    ! grep -Eiq '(password|token|secret)[[:space:]]*=[[:space:]]*[^_[:space:]#][^[:space:]]+' \
        "$SCRIPT_DIR/INPUTS.example.env" "$SCRIPT_DIR/templates/haproxy-redis-route.cfg"
    printf 'Q210_231_REDIS_STATIC_VALIDATE=PASS\n'
}

set_target() {
    case "$NODE" in
        redis01) EXPECTED_HOST=sqjcredis01; EXPECTED_IP=10.2.203.112; EXPECTED_ROLE=redis ;;
        redis02) EXPECTED_HOST=sqjcredis02; EXPECTED_IP=10.2.203.113; EXPECTED_ROLE=redis ;;
        nginx01) EXPECTED_HOST=sqjcnginx01; EXPECTED_IP=10.2.203.102; EXPECTED_ROLE=nginx ;;
        nginx02) EXPECTED_HOST=sqjcnginx02; EXPECTED_IP=10.2.203.103; EXPECTED_ROLE=nginx ;;
        *) die "unsupported node: $NODE" ;;
    esac
    case "$PHASE:$NODE" in
        q210-orchestrator:redis01|q230-redis5:redis01|q230-redis5:redis02|q230-redis5:nginx02|\
        q231-redis7:redis01|q231-redis7:redis02|q231-redis7:nginx02|\
        q231-db-proxy:nginx01|q231-db-proxy:nginx02) ;;
        *) die "phase/node combination is not permitted" ;;
    esac
}

assert_identity() {
    [[ $(hostname -s) == "$EXPECTED_HOST" ]] || die "hostname mismatch"
    ip -o -4 addr show scope global | awk '{split($4,a,"/");print a[1]}' |
        grep -Fxq "$EXPECTED_IP" || die "node IP mismatch"
    [[ -r /etc/zhct/node.env ]] || die "/etc/zhct/node.env missing"
    grep -Fxq "ROLE=$EXPECTED_ROLE" /etc/zhct/node.env
    grep -Fxq "NODE_KEY=$NODE" /etc/zhct/node.env
    grep -Fxq "NODE_IP=$EXPECTED_IP" /etc/zhct/node.env
    ! ip -o -4 addr show | grep -Eq '10\.2\.203\.(115|116|117)/' ||
        die "VIP present before Q-251"
}

load_inputs() {
    local key value
    [[ -r "$INPUT_FILE" ]] || die "--input-file is required"
    Q200_PASS_SHA= Q024_PASS_SHA= Q210_PASS_SHA= Q230_PASS_SHA=
    MAINTENANCE_ID= DATA_BACKUP_SHA= ALLOW_REPLICATION_RECONFIGURE=NO
    ALLOW_SENTINEL_ENABLE=NO HAPROXY_CONFIG_PATH=/etc/haproxy/haproxy.cfg
    while IFS='=' read -r key value; do
        [[ -z "$key" || "$key" == \#* ]] && continue
        [[ "$key" =~ ^[A-Z0-9_]+$ ]] || die "invalid input key"
        [[ "$value" != *'$('* && "$value" != *'`'* && "$value" != *';'* ]] ||
            die "unsafe value for $key"
        case "$key" in
            Q200_PASS_SHA|Q024_PASS_SHA|Q210_PASS_SHA|Q230_PASS_SHA|MAINTENANCE_ID|\
            DATA_BACKUP_SHA|ALLOW_REPLICATION_RECONFIGURE|ALLOW_SENTINEL_ENABLE|HAPROXY_CONFIG_PATH)
                printf -v "$key" '%s' "$value" ;;
            *) die "unknown input key: $key" ;;
        esac
    done <"$INPUT_FILE"
    valid_sha "$Q200_PASS_SHA" || die "Q200_PASS_SHA missing or invalid"
    valid_id "$MAINTENANCE_ID" || die "MAINTENANCE_ID missing or invalid"
    case "$PHASE" in
        q210-orchestrator) ;;
        q230-redis5)
            valid_sha "$Q210_PASS_SHA" || die "Q210_PASS_SHA missing or invalid"
            ;;
        q231-redis7|q231-db-proxy)
            valid_sha "$Q024_PASS_SHA" || die "Q024_PASS_SHA missing or invalid"
            valid_sha "$Q230_PASS_SHA" || die "Q230_PASS_SHA missing or invalid"
            ;;
    esac
}

assert_prerequisite_state() {
    if [[ "$NODE" == redis01 || "$NODE" == redis02 ]]; then
        systemctl is-active redis5.service >/dev/null 2>&1 || die "redis5.service not active"
        systemctl is-active redis7.service >/dev/null 2>&1 || die "redis7.service not active"
    fi
    case "$PHASE" in
        q210-orchestrator)
            [[ "$NODE" == redis01 ]]
            systemctl is-active sentinel5.service >/dev/null 2>&1 &&
                die "Sentinel5 must stay off before Q-230"
            systemctl is-active sentinel7.service >/dev/null 2>&1 &&
                die "Sentinel7 must stay off before Q-231"
            ;;
        q230-redis5)
            systemctl is-active sentinel7.service >/dev/null 2>&1 &&
                die "Sentinel7 must stay off before Q-231"
            ;;
        q231-db-proxy)
            systemctl is-enabled keepalived.service >/dev/null 2>&1 &&
                die "Keepalived must stay disabled before Q-251"
            systemctl is-active keepalived.service >/dev/null 2>&1 &&
                die "Keepalived must stay inactive before Q-251"
            ;;
    esac
}

backup_state() {
    BACKUP_DIR="$EVIDENCE_DIR/rollback"
    install -d -m 0700 "$BACKUP_DIR"
    for unit in orchestrator sentinel5 sentinel7 keepalived; do
        printf '%s|enabled=%s|active=%s\n' "$unit" \
            "$(systemctl is-enabled "$unit.service" 2>/dev/null || true)" \
            "$(systemctl is-active "$unit.service" 2>/dev/null || true)"
    done >"$BACKUP_DIR/unit-state-before.txt"
    case "$PHASE" in
        q210-orchestrator)
            install -m 0600 /etc/orchestrator.conf.json "$BACKUP_DIR/orchestrator.conf.json"
            ;;
        q231-db-proxy)
            if [[ -e /etc/zhct/staged/q231/haproxy-redis-route.cfg ]]; then
                install -m 0600 /etc/zhct/staged/q231/haproxy-redis-route.cfg \
                    "$BACKUP_DIR/haproxy-redis-route.cfg.before"
                printf 'present\n' >"$BACKUP_DIR/staged-before"
            else
                printf 'absent\n' >"$BACKUP_DIR/staged-before"
            fi
            ;;
    esac
}

redis_role() {
    local port=$1
    redis-cli -h 127.0.0.1 -p "$port" --raw INFO replication |
        awk -F: '$1=="role" {gsub(/\r/,"",$2);print $2;exit}'
}

verify_orchestrator_config() {
    python3 - /etc/orchestrator.conf.json <<'PY'
import json, sys
p=sys.argv[1]
d=json.load(open(p))
want={"10.2.203.102:10008","10.2.203.103:10008","10.2.203.112:10008"}
got=set(d.get("RaftNodes", []))
if got != want:
    raise SystemExit(f"RaftNodes mismatch: {sorted(got)}")
PY
}

preflight_phase() {
    assert_prerequisite_state
    case "$PHASE" in
        q210-orchestrator)
            verify_orchestrator_config
            [[ $(systemctl is-enabled orchestrator.service 2>/dev/null || true) == disabled ]]
            [[ $(systemctl is-active orchestrator.service 2>/dev/null || true) == inactive ]]
            ;;
        q230-redis5|q231-redis7)
            local ver port sentinel
            if [[ "$PHASE" == q230-redis5 ]]; then ver=5; port=6379; sentinel=sentinel5; else ver=7; port=6387; sentinel=sentinel7; fi
            if [[ "$NODE" == nginx02 ]]; then
                systemctl cat "$sentinel.service" >/dev/null
            else
                [[ -x "/opt/redis/$([[ $ver == 5 ]] && echo 5.0.14 || echo 7.2.14)/bin/redis-cli" ]]
                [[ $(redis_role "$port") == "$([[ $NODE == redis01 ]] && echo master || echo slave)" ]] ||
                    die "Redis role does not match node policy"
            fi
            ;;
        q231-db-proxy)
            [[ -r "$HAPROXY_CONFIG_PATH" ]] || die "live HAProxy config missing"
            grep -Fq '__REDIS5_PASSWORD__' "$SCRIPT_DIR/templates/haproxy-redis-route.cfg"
            grep -Fq '__REDIS7_PASSWORD__' "$SCRIPT_DIR/templates/haproxy-redis-route.cfg"
            ;;
    esac
}

execute_phase() {
    preflight_phase
    backup_state
    case "$PHASE" in
        q210-orchestrator)
            systemctl enable --now orchestrator.service
            ;;
        q230-redis5|q231-redis7)
            local port sentinel
            if [[ "$PHASE" == q230-redis5 ]]; then port=6379; sentinel=sentinel5; else port=6387; sentinel=sentinel7; fi
            [[ "$ALLOW_SENTINEL_ENABLE" == YES ]] || die "ALLOW_SENTINEL_ENABLE must be YES"
            if [[ "$NODE" == redis02 ]]; then
                valid_sha "$DATA_BACKUP_SHA" || die "DATA_BACKUP_SHA missing or invalid"
                [[ "$ALLOW_REPLICATION_RECONFIGURE" == YES ]] ||
                    die "ALLOW_REPLICATION_RECONFIGURE must be YES"
                redis-cli -h 127.0.0.1 -p "$port" REPLICAOF 10.2.203.112 "$port"
                printf 'changed\n' >"$BACKUP_DIR/data-path-change"
            fi
            systemctl enable --now "$sentinel.service"
            ;;
        q231-db-proxy)
            install -d -m 0750 /etc/zhct/staged/q231
            install -o root -g root -m 0640 "$SCRIPT_DIR/templates/haproxy-redis-route.cfg" \
                /etc/zhct/staged/q231/haproxy-redis-route.cfg
            ;;
    esac
    verify_phase
}

verify_phase() {
    case "$PHASE" in
        q210-orchestrator)
            verify_orchestrator_config
            systemctl is-enabled orchestrator.service
            systemctl is-active orchestrator.service
            ss -lntH | awk '{print $4}' | grep -Eq '(:|\])10008$'
            printf 'ORCHESTRATOR_SCOPE=LOCAL_THIRD_VOTER_ONLY_NO_QUORUM_CLAIM\n'
            ;;
        q230-redis5|q231-redis7)
            local port sentinel sport want_role
            if [[ "$PHASE" == q230-redis5 ]]; then port=6379; sentinel=sentinel5; sport=26379; else port=6387; sentinel=sentinel7; sport=26387; fi
            systemctl is-enabled "$sentinel.service"
            systemctl is-active "$sentinel.service"
            ss -lntH | awk '{print $4}' | grep -Eq "(:|\\])${sport}$"
            if [[ "$NODE" != nginx02 ]]; then
                want_role=$([[ "$NODE" == redis01 ]] && echo master || echo slave)
                [[ $(redis_role "$port") == "$want_role" ]] || die "Redis role verification failed"
            fi
            printf 'FAILOVER_EXECUTION=NOT_IMPLEMENTED\n'
            printf 'HA_PASS=NOT_CLAIMED_BY_SINGLE_NODE\n'
            ;;
        q231-db-proxy)
            cmp -s "$SCRIPT_DIR/templates/haproxy-redis-route.cfg" \
                /etc/zhct/staged/q231/haproxy-redis-route.cfg
            ! ip -o -4 addr show | grep -Fq '10.2.203.117/'
            [[ $(systemctl is-active keepalived.service 2>/dev/null || true) != active ]]
            printf 'HAPROXY_LIVE_CONFIG=UNCHANGED\nVIP=OFF_UNTIL_Q251\n'
            ;;
    esac
}

rollback_phase() {
    valid_id "$SOURCE_RUN_ID" || die "--source-run-id is required"
    local source="$EVIDENCE_BASE/$PHASE/$NODE/$SOURCE_RUN_ID/rollback"
    [[ -d "$source" ]] || die "rollback evidence missing"
    if [[ -e "$source/data-path-change" ]]; then
        die "automatic rollback refused after replication data-path change; approved recovery decision required"
    fi
    case "$PHASE" in
        q210-orchestrator) systemctl disable --now orchestrator.service ;;
        q230-redis5) systemctl disable --now sentinel5.service ;;
        q231-redis7) systemctl disable --now sentinel7.service ;;
        q231-db-proxy)
            if grep -Fxq present "$source/staged-before"; then
                install -o root -g root -m 0640 "$source/haproxy-redis-route.cfg.before" \
                    /etc/zhct/staged/q231/haproxy-redis-route.cfg
            else
                rm -f /etc/zhct/staged/q231/haproxy-redis-route.cfg
            fi
            ;;
    esac
    printf 'ROLLBACK_SCOPE=CONFIGURATION_AND_UNIT_ONLY\n'
}

archive_phase() {
    valid_id "$SOURCE_RUN_ID" || die "--source-run-id is required"
    local source="$EVIDENCE_BASE/$PHASE/$NODE/$SOURCE_RUN_ID"
    local archive="/root/redis-${PHASE}-${NODE}-evidence-${SOURCE_RUN_ID}.tar.gz"
    [[ -s "$source/stdout-stderr.log" && -s "$source/execution-receipt.env" && -s "$source/result.env" ]] ||
        die "source evidence incomplete"
    [[ ! -e "$archive" ]] || die "archive exists"
    tar -C "$(dirname "$source")" -czf "$archive" "$(basename "$source")"
    chmod 0600 "$archive"
    sha256sum "$archive" >"${archive}.sha256"
    chmod 0600 "${archive}.sha256"
    printf 'archive=%s\narchive_sha256=%s\nrc=0\nstate=PASS\n' "$archive" \
        "$(sha256sum "$archive" | awk '{print $1}')" >"${archive}.receipt.env"
    chmod 0600 "${archive}.receipt.env"
    printf 'Q210_231_REDIS_ARCHIVE=PASS\n'
}

if [[ ${1:-} == --static-validate ]]; then static_validate; exit 0; fi
PHASE=${1:-}; NODE=${2:-}; ACTION=${3:-}
[[ -n "$PHASE" && -n "$NODE" && -n "$ACTION" ]] || { usage; exit 2; }
shift 3
while (($#)); do
    case "$1" in
        --input-file) INPUT_FILE=${2:-}; shift 2 ;;
        --run-id) RUN_ID=${2:-}; shift 2 ;;
        --source-run-id) SOURCE_RUN_ID=${2:-}; shift 2 ;;
        -h|--help) usage; exit 0 ;;
        *) die "unknown option: $1" ;;
    esac
done
case "$ACTION" in preflight|execute|verify|rollback|archive) ;; *) die "invalid action" ;; esac
set_target
[[ $(id -u) -eq 0 ]] || die "remote actions require root"
for cmd in awk bash date grep hostname ip python3 sed sha256sum ss systemctl tar; do need "$cmd"; done
if [[ "$NODE" == redis01 || "$NODE" == redis02 ]]; then need redis-cli; fi
assert_identity
if [[ "$ACTION" == archive ]]; then archive_phase; exit 0; fi
[[ -n "$RUN_ID" ]] || RUN_ID="${PHASE}-${NODE}-${ACTION}-$(date +%Y%m%dT%H%M%S%z)"
valid_id "$RUN_ID" || die "invalid run ID"
EVIDENCE_DIR="$EVIDENCE_BASE/$PHASE/$NODE/$RUN_ID"
[[ ! -e "$EVIDENCE_DIR" ]] || die "evidence directory exists"
install -d -m 0700 "$EVIDENCE_DIR"
LOG_FILE="$EVIDENCE_DIR/stdout-stderr.log"
RESULT_FILE="$EVIDENCE_DIR/result.env"
RECEIPT_FILE="$EVIDENCE_DIR/execution-receipt.env"
STARTED_AT=$(timestamp)
install -m 0600 "$SCRIPT_DIR/SHA256SUMS" "$EVIDENCE_DIR/package-SHA256SUMS"
printf 'phase=%s\nnode=%s\naction=%s\ninput_file=%s\nrun_id=%s\n' \
    "$PHASE" "$NODE" "$ACTION" "$INPUT_FILE" "$RUN_ID" >"$EVIDENCE_DIR/invocation.env"
exec > >(sanitize | tee -a "$LOG_FILE") 2>&1
finish() {
    local rc=$?
    trap - EXIT
    local state=FAIL
    [[ "$rc" -eq 0 ]] && state=PASS
    [[ -s "$RESULT_FILE" ]] || {
        printf 'phase=%s\nnode=%s\naction=%s\nrun_id=%s\nrc=%s\nstate=%s\n' \
            "$PHASE" "$NODE" "$ACTION" "$RUN_ID" "$rc" "$state" >"$RESULT_FILE"
        printf 'vip=OFF_UNTIL_Q251\ncross_host_ha=NOT_CLAIMED\n' >>"$RESULT_FILE"
    }
    printf 'phase=%s\nnode=%s\ntarget_hostname=%s\ntarget_ip=%s\naction=%s\nrun_id=%s\nstarted_at=%s\nfinished_at=%s\nrc=%s\nrollback_state=%s\n' \
        "$PHASE" "$NODE" "$EXPECTED_HOST" "$EXPECTED_IP" "$ACTION" "$RUN_ID" \
        "$STARTED_AT" "$(timestamp)" "$rc" \
        "$([[ "$ACTION" == rollback ]] && echo ATTEMPTED || echo AVAILABLE_PER_POLICY)" >"$RECEIPT_FILE"
    chmod 0600 "$RESULT_FILE" "$RECEIPT_FILE"
    printf 'FINAL_RC=%s\n' "$rc"
    exit "$rc"
}
trap finish EXIT
load_inputs
log "phase=$PHASE node=$NODE action=$ACTION target=$EXPECTED_HOST/$EXPECTED_IP run_id=$RUN_ID"
case "$ACTION" in
    preflight) preflight_phase ;;
    execute) execute_phase ;;
    verify) verify_phase ;;
    rollback) rollback_phase ;;
esac
printf 'phase=%s\nnode=%s\naction=%s\nrun_id=%s\nrc=0\nstate=PASS\nvip=OFF_UNTIL_Q251\ncross_host_ha=NOT_CLAIMED\n' \
    "$PHASE" "$NODE" "$ACTION" "$RUN_ID" >"$RESULT_FILE"
printf 'Q210_231_REDIS_%s=PASS_WITH_PHASE_BOUNDARY\n' "${ACTION^^}"
