#!/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-q200/redis}
NODE=
ACTION=
INPUT_FILE=
RUN_ID=
SOURCE_RUN_ID=
STARTED_AT=

usage() {
    cat <<'USAGE'
Usage:
  redis-q200-gate --static-validate
  redis-q200-gate redis01|redis02 preflight|execute|verify|rollback [--input-file PATH] [--run-id ID]
  redis-q200-gate redis01|redis02 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}$ ]]; }
require_command() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }

sanitize_stream() {
    sed -E 's/([Pp]assword|[Tt]oken|[Cc]ookie|[Ss]ecret)([[:space:]]*[:=][[:space:]]*)[^[:space:]]+/\1\2[REDACTED]/g'
}

static_validate() {
    local expected
    [[ -f "$SCRIPT_DIR/SHA256SUMS" ]] || die "SHA256SUMS missing"
    (cd "$SCRIPT_DIR" && sha256sum -c SHA256SUMS)
    bash -n "$SCRIPT_DIR/redis-q200-gate"
    grep -Fqx $'redis01\t10.2.203.112\tsqjcredis01\tredis' "$SCRIPT_DIR/manifests/hosts.tsv"
    grep -Fqx $'redis02\t10.2.203.113\tsqjcredis02\tredis' "$SCRIPT_DIR/manifests/hosts.tsv"
    grep -Fqx $'6379\ttcp\tnode-ip\tnginx01,nginx02,redis-peer,sentinels,management\tRedis5' "$SCRIPT_DIR/manifests/ports.tsv"
    grep -Fqx $'10008\ttcp\tnode-ip\tnginx01,nginx02,redis01\tOrchestrator Raft on redis01 only' "$SCRIPT_DIR/manifests/ports.tsv"
    expected=$(awk -F'\t' 'NR > 1 {print $1}' "$SCRIPT_DIR/manifests/hosts.tsv" | sort -u | wc -l)
    [[ "$expected" -eq 11 ]] || die "hosts manifest must contain 11 unique nodes"
    [[ $(awk -F'\t' 'NR > 1 {print $1}' "$SCRIPT_DIR/manifests/ports.tsv" | sort -nu | wc -l) -eq 6 ]] ||
        die "ports manifest must contain 6 unique ports"
    grep -Fq 'Q194_PASS_SHA=' "$SCRIPT_DIR/INPUTS.example.env"
    grep -Fq 'MANAGEMENT_CIDRS=' "$SCRIPT_DIR/INPUTS.example.env"
    grep -Fq 'CROSS_SUBNET_GATEWAY=' "$SCRIPT_DIR/INPUTS.example.env"
    printf 'Q200_REDIS_STATIC_VALIDATE=PASS\n'
}

set_node_policy() {
    case "$NODE" in
        redis01)
            EXPECTED_HOST=sqjcredis01
            EXPECTED_IP=10.2.203.112
            ;;
        redis02)
            EXPECTED_HOST=sqjcredis02
            EXPECTED_IP=10.2.203.113
            ;;
        *) die "node must be redis01 or redis02" ;;
    esac
}

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

load_inputs() {
    local key value
    [[ -r "$INPUT_FILE" ]] || die "--input-file is required and must be readable"
    : "${Q194_PASS_SHA:=}" "${FIREWALL_ZONE:=}" "${MANAGEMENT_CIDRS:=}"
    : "${CONTROL_CIDRS:=}" "${NTP_SERVERS:=}" "${DNS_SERVERS:=}"
    : "${CROSS_SUBNET_GATEWAY:=}"
    while IFS='=' read -r key value; do
        [[ -z "$key" || "$key" == \#* ]] && continue
        [[ "$key" =~ ^[A-Z0-9_]+$ ]] || die "invalid input key"
        [[ "$value" != *'$('* && "$value" != *'`'* && "$value" != *';'* ]] ||
            die "unsafe input value for $key"
        case "$key" in
            Q194_PASS_SHA) Q194_PASS_SHA=$value ;;
            FIREWALL_ZONE) FIREWALL_ZONE=$value ;;
            MANAGEMENT_CIDRS) MANAGEMENT_CIDRS=$value ;;
            CONTROL_CIDRS) CONTROL_CIDRS=$value ;;
            NTP_SERVERS) NTP_SERVERS=$value ;;
            DNS_SERVERS) DNS_SERVERS=$value ;;
            CROSS_SUBNET_GATEWAY) CROSS_SUBNET_GATEWAY=$value ;;
            *) die "unknown input key: $key" ;;
        esac
    done <"$INPUT_FILE"
    valid_sha "$Q194_PASS_SHA" || die "Q194_PASS_SHA missing or invalid"
    [[ -n "$FIREWALL_ZONE" ]] || die "FIREWALL_ZONE missing"
    [[ -n "$MANAGEMENT_CIDRS" ]] || die "MANAGEMENT_CIDRS missing"
    [[ -n "$CONTROL_CIDRS" ]] || die "CONTROL_CIDRS missing"
    [[ -n "$NTP_SERVERS" ]] || die "NTP_SERVERS missing"
    [[ -n "$DNS_SERVERS" ]] || die "DNS_SERVERS missing"
    [[ -n "$CROSS_SUBNET_GATEWAY" ]] || die "CROSS_SUBNET_GATEWAY missing"
}

capture_identity_time() {
    local expected item
    printf '%s\n' '=== TARGET ==='
    date --iso-8601=seconds
    hostname -s
    ip -o -4 addr show scope global
    grep -E '^(ROLE|NODE_KEY|NODE_IP|NODE_NAME|APPLIED_AT)=' /etc/zhct/node.env
    printf '%s\n' '=== TIME ==='
    timedatectl status
    timedatectl show -p NTPSynchronized -p TimeUSec -p Timezone
    [[ $(timedatectl show -p NTPSynchronized --value) == yes ]] ||
        die "system clock is not NTP synchronized"
    if command -v chronyc >/dev/null 2>&1; then
        chronyc tracking
        chronyc sources -v
        expected=$(chronyc sources -n 2>/dev/null || true)
        IFS=',' read -ra items <<<"$NTP_SERVERS"
        for item in "${items[@]}"; do
            grep -Fq "$item" <<<"$expected" || die "expected NTP source not observed: $item"
        done
    fi
    IFS=',' read -ra items <<<"$DNS_SERVERS"
    for item in "${items[@]}"; do
        grep -Eq "^[[:space:]]*nameserver[[:space:]]+$item([[:space:]]|$)" /etc/resolv.conf ||
            die "expected DNS server not configured: $item"
    done
}

check_resolution_routes() {
    local node ip host role resolved
    printf '%s\n' '=== HOST RESOLUTION / ROUTES ==='
    while IFS=$'\t' read -r node ip host role; do
        [[ "$node" == node_key ]] && continue
        resolved=$(getent ahostsv4 "$host" | awk 'NR == 1 {print $1}')
        printf '%s|expected=%s|resolved=%s|role=%s\n' "$host" "$ip" "$resolved" "$role"
        [[ "$resolved" == "$ip" ]] || die "resolution mismatch for $host"
        ip route get "$ip"
    done <"$SCRIPT_DIR/manifests/hosts.tsv"
    ip route get 10.2.208.106 | grep -Fq "via $CROSS_SUBNET_GATEWAY" ||
        die "cross-subnet route does not use expected gateway"
}

check_firewalld() {
    local port source rule missing=0
    local -a sources
    printf '%s\n' '=== FIREWALLD SNAPSHOT ==='
    firewall-cmd --state
    firewall-cmd --get-active-zones
    firewall-cmd --zone="$FIREWALL_ZONE" --list-all
    firewall-cmd --permanent --zone="$FIREWALL_ZONE" --list-all

    case "$NODE" in
        redis01)
            sources=(10.2.203.102/32 10.2.203.103/32 10.2.203.112/32 10.2.203.113/32)
            ;;
        redis02)
            sources=(10.2.203.102/32 10.2.203.103/32 10.2.203.112/32 10.2.203.113/32)
            ;;
    esac
    IFS=',' read -ra extra <<<"$MANAGEMENT_CIDRS"
    sources+=("${extra[@]}")
    for port in 6379 6387 26379 26387; do
        for source in "${sources[@]}"; do
            rule="rule family=ipv4 source address=$source port port=$port protocol=tcp accept"
            if ! firewall-cmd --quiet --zone="$FIREWALL_ZONE" --query-rich-rule="$rule"; then
                printf 'MISSING_RUNTIME_RULE|%s\n' "$rule"
                missing=1
            fi
            if ! firewall-cmd --quiet --permanent --zone="$FIREWALL_ZONE" --query-rich-rule="$rule"; then
                printf 'MISSING_PERMANENT_RULE|%s\n' "$rule"
                missing=1
            fi
        done
    done
    if [[ "$NODE" == redis01 ]]; then
        for source in 10.2.203.102/32 10.2.203.103/32 10.2.203.112/32; do
            rule="rule family=ipv4 source address=$source port port=10008 protocol=tcp accept"
            firewall-cmd --quiet --zone="$FIREWALL_ZONE" --query-rich-rule="$rule" ||
                { printf 'MISSING_RUNTIME_RULE|%s\n' "$rule"; missing=1; }
            firewall-cmd --quiet --permanent --zone="$FIREWALL_ZONE" --query-rich-rule="$rule" ||
                { printf 'MISSING_PERMANENT_RULE|%s\n' "$rule"; missing=1; }
        done
        IFS=',' read -ra extra <<<"$CONTROL_CIDRS"
        for source in "${extra[@]}"; do
            rule="rule family=ipv4 source address=$source port port=3000 protocol=tcp accept"
            firewall-cmd --quiet --zone="$FIREWALL_ZONE" --query-rich-rule="$rule" ||
                { printf 'MISSING_RUNTIME_RULE|%s\n' "$rule"; missing=1; }
            firewall-cmd --quiet --permanent --zone="$FIREWALL_ZONE" --query-rich-rule="$rule" ||
                { printf 'MISSING_PERMANENT_RULE|%s\n' "$rule"; missing=1; }
        done
    fi
    [[ "$missing" -eq 0 ]] || die "required firewalld rules are incomplete"
}

check_connectivity() {
    local source target ip port purpose failures=0
    printf '%s\n' '=== TCP CONNECTIVITY ==='
    while IFS=$'\t' read -r source target ip port purpose; do
        [[ "$source" == source_node || "$source" != "$NODE" ]] && continue
        if timeout 3 bash -c ":</dev/tcp/$ip/$port" 2>/dev/null; then
            printf 'PASS|%s|%s:%s|%s\n' "$target" "$ip" "$port" "$purpose"
        else
            printf 'FAIL|%s|%s:%s|%s\n' "$target" "$ip" "$port" "$purpose"
            failures=1
        fi
    done <"$SCRIPT_DIR/manifests/connectivity.tsv"
    [[ "$failures" -eq 0 ]] || die "one or more required TCP paths failed"
}

assert_sentinel_vip_off() {
    local unit
    for unit in sentinel5 sentinel7; do
        [[ $(systemctl is-enabled "$unit.service" 2>/dev/null || true) == disabled ]] ||
            die "$unit.service must stay disabled before Q-230"
        [[ $(systemctl is-active "$unit.service" 2>/dev/null || true) == inactive ]] ||
            die "$unit.service must stay inactive before Q-230"
    done
    ! ip -o -4 addr show | grep -Eq '10\.2\.203\.(115|116|117)/' ||
        die "VIP must stay absent before Q-251"
}

write_result() {
    local state=$1 detail=$2 rc=$3
    {
        printf 'node=%s\n' "$NODE"
        printf 'hostname=%s\n' "$EXPECTED_HOST"
        printf 'ip=%s\n' "$EXPECTED_IP"
        printf 'action=%s\n' "$ACTION"
        printf 'run_id=%s\n' "$RUN_ID"
        printf 'started_at=%s\n' "$STARTED_AT"
        printf 'finished_at=%s\n' "$(timestamp)"
        printf 'rc=%s\n' "$rc"
        printf 'state=%s\n' "$state"
        printf 'detail=%s\n' "$detail"
        printf 'configuration_changes=NONE_READ_ONLY_GATE\n'
        printf 'sentinel=DISABLED_INACTIVE_UNTIL_Q230\n'
        printf 'vip=ABSENT_UNTIL_Q251\n'
        printf 'cross_host_ha=NOT_CLAIMED\n'
    } >"$RESULT_FILE"
    chmod 0600 "$RESULT_FILE"
}

archive_action() {
    valid_id "$SOURCE_RUN_ID" || die "--source-run-id missing or invalid"
    assert_identity
    local source_dir="$EVIDENCE_BASE/$NODE/$SOURCE_RUN_ID"
    local archive="/root/redis-q200-${NODE}-evidence-${SOURCE_RUN_ID}.tar.gz"
    [[ -d "$source_dir" ]] || die "source evidence missing: $source_dir"
    [[ -s "$source_dir/stdout-stderr.log" ]] || die "stdout/stderr evidence missing"
    [[ -s "$source_dir/execution-receipt.env" ]] || die "RC/time/target receipt missing"
    [[ -s "$source_dir/result.env" ]] || die "result missing"
    [[ ! -e "$archive" ]] || die "archive exists: $archive"
    tar -C "$(dirname "$source_dir")" -czf "$archive" "$(basename "$source_dir")"
    chmod 0600 "$archive"
    (cd /root && sha256sum "$(basename "$archive")" >"$(basename "$archive").sha256")
    chmod 0600 "${archive}.sha256"
    {
        printf 'source_run_id=%s\n' "$SOURCE_RUN_ID"
        printf 'node=%s\n' "$NODE"
        printf 'target_hostname=%s\n' "$EXPECTED_HOST"
        printf 'target_ip=%s\n' "$EXPECTED_IP"
        printf 'archived_at=%s\n' "$(timestamp)"
        printf 'archive=%s\n' "$archive"
        printf 'archive_sha256=%s\n' "$(sha256sum "$archive" | awk '{print $1}')"
        printf 'rc=0\n'
        printf 'state=PASS\n'
        printf 'rollback_state=NO_CONFIGURATION_CHANGE\n'
    } >"${archive}.receipt.env"
    chmod 0600 "${archive}.receipt.env"
    sha256sum "$archive"
    printf 'Q200_REDIS_ARCHIVE=PASS\n'
}

if [[ ${1:-} == --static-validate ]]; then
    static_validate
    exit 0
fi

NODE=${1:-}
ACTION=${2:-}
[[ -n "$NODE" && -n "$ACTION" ]] || { usage; exit 2; }
shift 2
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_node_policy
[[ $(id -u) -eq 0 ]] || die "remote actions require root"
for cmd in awk bash date firewall-cmd getent grep hostname ip sed sha256sum \
    systemctl tar timedatectl timeout; do require_command "$cmd"; done

if [[ "$ACTION" == archive ]]; then
    archive_action
    exit 0
fi

[[ -n "$RUN_ID" ]] || RUN_ID="q200-${NODE}-${ACTION}-$(date +%Y%m%dT%H%M%S%z)"
valid_id "$RUN_ID" || die "invalid run ID"
EVIDENCE_DIR="$EVIDENCE_BASE/$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 'program=%s\n' "$PROGRAM"
    printf 'node=%s\n' "$NODE"
    printf 'action=%s\n' "$ACTION"
    printf 'input_file=%s\n' "$INPUT_FILE"
    printf 'run_id=%s\n' "$RUN_ID"
} >"$EVIDENCE_DIR/invocation.env"
exec > >(sanitize_stream | tee -a "$LOG_FILE") 2>&1

finish() {
    local rc=$?
    trap - EXIT
    [[ -s "$RESULT_FILE" ]] || write_result FAIL "unhandled_rc_$rc" "$rc"
    {
        printf 'node=%s\n' "$NODE"
        printf 'action=%s\n' "$ACTION"
        printf 'run_id=%s\n' "$RUN_ID"
        printf 'target_hostname=%s\n' "$EXPECTED_HOST"
        printf 'target_ip=%s\n' "$EXPECTED_IP"
        printf 'started_at=%s\n' "$STARTED_AT"
        printf 'finished_at=%s\n' "$(timestamp)"
        printf 'rc=%s\n' "$rc"
        printf 'stdout_stderr=%s\n' "$LOG_FILE"
        printf 'rollback_state=NO_CONFIGURATION_CHANGE\n'
    } >"$RECEIPT_FILE"
    chmod 0600 "$RECEIPT_FILE"
    printf 'FINAL_RC=%s\n' "$rc"
    exit "$rc"
}
trap finish EXIT

log "node=$NODE action=$ACTION target=$EXPECTED_HOST/$EXPECTED_IP run_id=$RUN_ID"
assert_identity
load_inputs
capture_identity_time
assert_sentinel_vip_off

case "$ACTION" in
    preflight)
        write_result PASS PREFLIGHT_INPUTS_AND_IDENTITY_ONLY 0
        printf 'Q200_REDIS_PREFLIGHT=PASS_READ_ONLY\n'
        ;;
    execute|verify)
        check_resolution_routes
        check_firewalld
        check_connectivity
        assert_sentinel_vip_off
        write_result PASS NETWORK_GATE_READ_ONLY 0
        printf 'Q200_REDIS_%s=PASS_READ_ONLY_GATE\n' "${ACTION^^}"
        ;;
    rollback)
        assert_sentinel_vip_off
        write_result PASS NO_CONFIGURATION_TO_ROLL_BACK 0
        printf 'Q200_REDIS_ROLLBACK=PASS_NOOP_READ_ONLY\n'
        ;;
esac
