#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")
ORIGINAL_ARGS=("$@")
AUDIT_ROOT=${Q160_AUDIT_ROOT:-/root/q160-nginx-readonly-audit}

usage() {
    cat <<'USAGE'
Usage:
  q160-nginx-role-readonly.sh --static-validate
  q160-nginx-role-readonly.sh run --node nginx01|nginx02
USAGE
}

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

need() {
    command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
}

mode=${1:-}
node=
if [[ "$mode" == --static-validate && $# -eq 1 ]]; then
    target_host=LOCAL_STATIC_ONLY
    target_ip=LOCAL_STATIC_ONLY
elif [[ "$mode" == run && $# -eq 3 && ${2:-} == --node ]]; then
    node=$3
    case "$node" in
        nginx01)
            target_host=sqjcnginx01
            target_ip=10.2.203.102
            expected_raft=true
            ;;
        nginx02)
            target_host=sqjcnginx02
            target_ip=10.2.203.103
            expected_raft=false
            ;;
        *)
            die "unsupported node: $node"
            exit 2
            ;;
    esac
else
    usage
    exit 2
fi

run_id=${Q160_RUN_ID:-}
if [[ "$mode" == --static-validate ]]; then
    run_id=${run_id:-q160-static-validate}
    audit_log=/dev/null
else
    [[ "$run_id" =~ ^q160-[A-Za-z0-9._+-]+$ ]] || die 'invalid or missing Q160_RUN_ID'
    mkdir -p "$AUDIT_ROOT"
    chmod 0700 "$AUDIT_ROOT"
    audit_log="$AUDIT_ROOT/${run_id}-${node}.log"
fi

if [[ "$mode" == run && ${Q160_LOG_WRAPPED:-0} != 1 ]]; then
    export Q160_LOG_WRAPPED=1
    set +e
    "$SCRIPT_PATH" "${ORIGINAL_ARGS[@]}" 2>&1 | tee -a "$audit_log"
    wrapped_rc=${PIPESTATUS[0]}
    exit "$wrapped_rc"
fi

finish() {
    local rc=$?
    printf 'Q160_MODE_END=%s\n' "$mode"
    printf 'Q160_END_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
    printf 'Q160_TARGET_HOST=%s\n' "$target_host"
    printf 'Q160_TARGET_IP=%s\n' "$target_ip"
    printf 'Q160_TARGET_NODE=%s\n' "${node:-STATIC}"
    printf 'Q160_RC=%s\n' "$rc"
    printf 'Q160_ROLLBACK_STATE=NO_SYSTEM_CHANGE_TO_ROLL_BACK\n'
    if [[ "$rc" -eq 0 && "$mode" == --static-validate ]]; then
        printf 'Q160_JUDGMENT=LOCAL_STATIC_ONLY_REMOTE_NOT_EXECUTED\n'
    elif [[ "$rc" -eq 0 ]]; then
        printf 'Q160_JUDGMENT=NODE_READONLY_PASS_NOT_GLOBAL_Q160_PASS\n'
    else
        printf 'Q160_JUDGMENT=FAIL_CLOSED\n'
    fi
    return "$rc"
}
trap finish EXIT

printf 'Q160_MODE_BEGIN=%s\n' "$mode"
printf 'Q160_START_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'Q160_TARGET_HOST=%s\n' "$target_host"
printf 'Q160_TARGET_IP=%s\n' "$target_ip"
printf 'Q160_TARGET_NODE=%s\n' "${node:-STATIC}"
printf 'Q160_RUN_ID=%s\n' "$run_id"

static_validate() {
    bash -n "$SCRIPT_PATH"
    for required in --static-validate run nginx01 nginx02 Q160_DEP_Q143 Q160_REMOTE_LOCK_GRANTED; do
        grep -Fq -- "$required" "$SCRIPT_PATH" || die "missing marker: $required"
    done
    printf 'Q160_LOCAL_STATIC_VALIDATE_PASS\n'
    printf 'Q160_REMOTE_STATUS=NOT_EXECUTED\n'
}

dependency_lock_gate() {
    [[ ${Q160_DEP_Q143:-} == PASS ]] || die 'Q-143 PASS is not asserted'
    [[ ${Q160_DEP_Q101:-} == PASS ]] || die 'Q-101 PASS is not asserted'
    [[ ${Q160_DEP_Q102:-} == PASS ]] || die 'Q-102 PASS is not asserted'
    [[ ${Q160_REMOTE_LOCK_GRANTED:-} == 1 ]] || die 'remote lock is not asserted'
    [[ ${Q160_REMOTE_LOCK_OWNER:-} == NGINX_Q160 ]] || die 'remote lock owner mismatch'
    printf 'Q160_DEPENDENCY_LOCK_GATE=PASS\n'
}

identity_gate() {
    [[ $(id -u) -eq 0 ]] || die 'root is required'
    [[ "$(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 'node.env is absent'
    grep -Fxq 'ROLE=nginx' /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'
    grep -Fxq "NODE_NAME=$target_host" /etc/zhct/node.env || die 'node name mismatch'
    printf 'Q160_IDENTITY_GATE=PASS\n'
}

role_asset_gate() {
    local base=/opt/zhct-deploy/roles/nginx
    local expected actual relative
    while read -r expected relative; do
        [[ -r "$base/$relative" ]] || die "missing role asset: $relative"
        actual=$(sha256sum "$base/$relative" | awk '{print $1}')
        printf 'ROLE_ASSET=%s EXPECTED_SHA256=%s ACTUAL_SHA256=%s\n' \
            "$relative" "$expected" "$actual"
        [[ "$actual" == "$expected" ]] || die "role asset digest mismatch: $relative"
    done <<'ASSETS'
92c3b974b74c9d128c52a989624c97a1bb6ba77057dd02cf6de6421d3087f105 hooks.sh
515e68791bd62b760e6dd5707d0579dd03ababcb51afbcc90903c69836e37f6d role.env
ce18e7febc7e3105294ddc74d32f10f7fb4da2c4d8e59c3e9642816ec770463f manifests/nodes.tsv
24ce7c17d7a7cf8070d1cdd9013c54e49a445cd264a0ecafcae4ef1912811d61 manifests/systemd-units.tsv
f99184afaa5e8c616e8982f9326ecfbb3a2e44af2ac6cbfee1cb7c2e6cd8c13c manifests/ports.tsv
ASSETS
    printf 'Q160_ROLE_ASSET_DIGEST_GATE=PASS\n'
}

identity_evidence() {
    printf 'Q160_NODE_ENV_BEGIN\n'
    grep -E '^(ROLE|NODE_KEY|NODE_IP|NODE_NAME|APPLIED_AT)=' /etc/zhct/node.env
    printf 'Q160_NODE_ENV_END\n'
    stat -c 'MACHINE_ID mode=%a owner=%U:%G size=%s' /etc/machine-id
    [[ $(wc -c </etc/machine-id) -eq 33 ]] || die 'machine-id size is not 33 bytes'
    find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*.pub' -printf '%f %s\n' | sort
    [[ $(find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*.pub' | wc -l) -ge 3 ]] ||
        die 'SSH host public keys are incomplete'
    printf 'Q160_MACHINE_AND_SSH_IDENTITY_PRESENT=PASS\n'
}

service_boundary_gate() {
    local unit enabled active
    for unit in nginx haproxy orchestrator; 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" == enabled && "$active" == active ]] ||
            die "$unit must be enabled/active"
    done
    for unit in sentinel5 sentinel7 keepalived; 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
    if ip -o -4 addr show | grep -Eq '10\.2\.203\.(115|116|117)/'; then
        die 'VIP is present before Q-251'
    fi
    printf 'Q160_VIP_ABSENT=PASS\n'
}

nonsecret_role_identity() {
    python3 - "$target_ip" "$expected_raft" <<'PY'
import json
import sys

target_ip = sys.argv[1]
expected_raft = sys.argv[2] == "true"
with open("/etc/orchestrator.conf.json", encoding="utf-8") as handle:
    config = json.load(handle)
safe_keys = (
    "ListenAddress", "BackendDB", "SQLite3DataFile", "RaftEnabled",
    "RaftDataDir", "RaftBind", "DefaultRaftPort", "RaftNodes",
)
for key in safe_keys:
    print(f"ORCHESTRATOR_{key}={config.get(key)!r}")
assert config.get("ListenAddress") == f"{target_ip}:3000"
assert config.get("RaftBind") == target_ip
assert config.get("RaftEnabled") is expected_raft
PY
    printf 'KEEPALIVED_SAFE_IDENTITY_BEGIN\n'
    grep -E '^[[:space:]]*(router_id|priority|unicast_src_ip|unicast_peer)' \
        /etc/keepalived/keepalived.conf || true
    printf 'KEEPALIVED_SAFE_IDENTITY_END\n'
    printf 'Q160_NONSECRET_SOFTWARE_IDENTITY=PASS\n'
}

listener_gate() {
    ss -lntup
    ss -lntH | grep -Eq '0\.0\.0\.0:8081[[:space:]]' ||
        die 'nginx 8081 listener is absent'
    ss -lntH | grep -Eq "${target_ip}:3000[[:space:]]" ||
        die 'orchestrator 3000 listener is absent'
    if [[ "$node" == nginx02 ]] &&
        ss -lntH | grep -Eq "${target_ip}:10008[[:space:]]"; then
        die 'Q-210 Raft listener is present on nginx02 before Q-210'
    fi
    printf 'Q160_LISTENER_ROLE_BOUNDARY=PASS\n'
}

run_gate() {
    dependency_lock_gate
    for command in awk bash cut date find grep hostname id ip python3 sha256sum ss stat systemctl tee wc; do
        need "$command"
    done
    identity_gate
    role_asset_gate
    identity_evidence
    service_boundary_gate
    nonsecret_role_identity
    listener_gate
    printf 'Q160_NGINX_NODE_READONLY_PASS\n'
    printf 'Q160_GLOBAL_PASS=NOT_ASSERTED_BY_NODE_SCRIPT\n'
}

case "$mode" in
    --static-validate) static_validate ;;
    run) run_gate ;;
esac
