#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
TARGETS="$SCRIPT_DIR/q200-network-targets.tsv"
REMOTE_EVIDENCE_ROOT=/var/backups/zhct-q200/evidence
ORIGINAL_ARGS=("$@")

usage() {
    cat <<'USAGE'
Usage:
  q200-nginx-network-gate.sh --static-validate
  q200-nginx-network-gate.sh preflight|execute|verify|rollback|archive --node nginx01|nginx02
USAGE
}

die() { printf '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
    :
elif [[ $# -eq 3 && ${2:-} == --node ]]; then
    node=$3
else
    usage
    exit 2
fi
case "$mode" in
    --static-validate|preflight|execute|verify|rollback|archive) ;;
    *) usage; exit 2 ;;
esac

case "$node" in
    nginx01) target_host=sqjcnginx01; target_ip=10.2.203.102 ;;
    nginx02) target_host=sqjcnginx02; target_ip=10.2.203.103 ;;
    "") target_host=LOCAL_STATIC_ONLY; target_ip=LOCAL_STATIC_ONLY ;;
    *) die "unsupported node: $node" ;;
esac

run_id=${Q200_RUN_ID:-}
if [[ "$mode" == --static-validate ]]; then
    run_id=${run_id:-q200-static-validate}
    evidence_root=${Q200_EVIDENCE_ROOT:-$(mktemp -d "${TMPDIR:-/tmp}/q200-nginx-static.XXXXXX")}
else
    [[ "$run_id" =~ ^q200-[A-Za-z0-9._+-]+$ ]] || die 'invalid or missing Q200_RUN_ID'
    evidence_root=${Q200_EVIDENCE_ROOT:-$REMOTE_EVIDENCE_ROOT}
fi
evidence_dir="$evidence_root/$run_id/$node"
mkdir -p "$evidence_dir"
stamp=$(date -u +%Y%m%dT%H%M%SZ)
log=${Q200_LOG_FILE:-"$evidence_dir/$stamp-${mode#--}.log"}
rc_file=${Q200_RC_FILE:-"$evidence_dir/$stamp-${mode#--}.rc"}
if [[ ${Q200_LOG_WRAPPED:-0} != 1 ]]; then
    export Q200_LOG_WRAPPED=1 Q200_LOG_FILE="$log" Q200_RC_FILE="$rc_file"
    export Q200_EVIDENCE_ROOT="$evidence_root" Q200_RUN_ID="$run_id"
    set +e
    "$SCRIPT_PATH" "${ORIGINAL_ARGS[@]}" 2>&1 | tee -a "$log"
    wrapped_rc=${PIPESTATUS[0]}
    exit "$wrapped_rc"
fi

finish() {
    local rc=$?
    {
        printf 'Q200_MODE_END=%s\n' "$mode"
        printf 'Q200_END_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
        printf 'Q200_TARGET_HOST=%s\n' "$target_host"
        printf 'Q200_TARGET_IP=%s\n' "$target_ip"
        printf 'Q200_TARGET_NODE=%s\n' "${node:-STATIC}"
        printf 'Q200_RC=%s\n' "$rc"
        if [[ "$rc" -eq 0 ]]; then
            printf 'Q200_JUDGMENT=MODE_PASS_NOT_GLOBAL_Q200_PASS\n'
        else
            printf 'Q200_JUDGMENT=FAIL_CLOSED\n'
        fi
        printf 'Q200_ROLLBACK_STATE=NO_NETWORK_OR_SERVICE_CHANGE_BY_THIS_PACKAGE\n'
    } | tee -a "$rc_file"
    return "$rc"
}
trap finish EXIT

printf 'Q200_MODE_BEGIN=%s\n' "$mode"
printf 'Q200_START_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'Q200_TARGET_HOST=%s\n' "$target_host"
printf 'Q200_TARGET_IP=%s\n' "$target_ip"
printf 'Q200_TARGET_NODE=%s\n' "${node:-STATIC}"
printf 'Q200_RUN_ID=%s\n' "$run_id"

static_validate() {
    bash -n "$SCRIPT_PATH"
    (cd "$SCRIPT_DIR" && sha256sum -c SHA256SUMS)
    awk -F '\t' 'NR==1 { exit !($1=="role" && $4=="ip" && $5=="ports") }' "$TARGETS"
    grep -Fq 'Q200_EXECUTION_SCOPE=READ_ONLY_GATE_ONLY' "$SCRIPT_DIR/README.md"
    for required in --static-validate preflight execute verify rollback archive; do
        grep -Fq -- "$required" "$SCRIPT_PATH" || die "missing mode: $required"
    done
    if grep -RIEq '(password[[:space:]]*=|token[[:space:]]*=|BEGIN (RSA |OPENSSH )?PRIVATE KEY)' \
        "$SCRIPT_DIR" --exclude=SHA256SUMS; then
        die 'secret-like material detected'
    fi
    printf 'Q200_LOCAL_STATIC_VALIDATE_PASS\n'
    printf 'Q200_REMOTE_STATUS=NOT_EXECUTED\n'
}

lock_identity_only() {
    [[ $(id -u) -eq 0 ]] || die 'root is required'
    [[ ${Q200_REMOTE_LOCK_GRANTED:-} == 1 ]] || die 'remote lock is not asserted'
    [[ ${Q200_REMOTE_LOCK_OWNER:-} == NGINX_Q200 ]] || die 'remote lock owner mismatch'
    [[ "$(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 'missing /etc/zhct/node.env'
    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'
    printf 'Q200_LOCK_IDENTITY_GATE=PASS\n'
}

lock_identity_gate() {
    lock_identity_only
    [[ ${Q200_DEP_Q101:-} == PASS ]] || die 'Q-101 PASS is not asserted'
    [[ ${Q200_DEP_Q102:-} == PASS ]] || die 'Q-102 PASS is not asserted'
    [[ ${Q200_DEP_Q194:-} == PASS ]] || die 'Q-194 PASS is not asserted'
    printf 'Q200_LOCK_IDENTITY_DEPENDENCY_GATE=PASS\n'
}

source_policy_gate() {
    local file=${Q200_APPROVED_SOURCES_FILE:-}
    [[ -n "$file" && -f "$file" ]] || die 'approved source CIDR file is missing'
    [[ $(stat -c '%U:%G' "$file") == root:root ]] || die 'source CIDR file owner mismatch'
    case "$(stat -c '%a' "$file")" in 600|400) ;; *) die 'source CIDR file mode must be 0600 or 0400' ;; esac
    if grep -Eq '(^|[[:space:],])(0\.0\.0\.0/0|::/0)($|[[:space:],])' "$file"; then
        die 'unrestricted source CIDR is forbidden'
    fi
    local label
    for label in business-net device-and-app-net app-nodes inventory-app management-and-control; do
        awk -F '\t' -v want="$label" '$1==want && $2!="" {found=1} END {exit !found}' "$file" ||
            die "approved CIDR missing for $label"
    done
    printf 'Q200_APPROVED_SOURCE_POLICY=METADATA_PRESENT_NO_VALUES_LOGGED\n'
}

assert_service_boundary() {
    local unit enabled active
    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
    ! ip -o -4 addr show | grep -Eq '10\.2\.203\.(115|116|117)/' ||
        die 'VIP is present before Q-251'
    printf 'Q200_VIP_ABSENT=PASS\n'
}

time_hosts_routes_firewall() {
    timedatectl show -p NTPSynchronized -p NTP -p Timezone
    [[ "$(timedatectl show -p NTPSynchronized --value)" == yes ]] ||
        die 'clock is not synchronized'
    if command -v chronyc >/dev/null 2>&1; then chronyc tracking; fi

    while IFS=$'\t' read -r role peer peer_host peer_ip ports stage; do
        [[ "$role" == role ]] && continue
        printf 'PEER_BEGIN=%s HOST=%s IP=%s STAGE=%s PORTS=%s\n' \
            "$peer" "$peer_host" "$peer_ip" "$stage" "$ports"
        getent hosts "$peer_host" | awk -v ip="$peer_ip" '$1==ip {ok=1} END {exit !ok}' ||
            die "hosts/DNS mismatch for $peer_host"
        ip route get "$peer_ip"
    done <"$TARGETS"
    printf 'Q200_HOSTS_AND_ROUTES=PASS\n'

    need firewall-cmd
    firewall-cmd --state
    firewall-cmd --get-active-zones
    firewall-cmd --list-all-zones
    source_policy_gate
    printf 'Q200_FIREWALL_SNAPSHOT_CAPTURED=PASS\n'
}

read_only_tcp_probes() {
    local failures=0 role peer peer_host peer_ip ports stage port
    while IFS=$'\t' read -r role peer peer_host peer_ip ports stage; do
        [[ "$role" == role ]] && continue
        IFS=, read -r -a port_list <<<"$ports"
        for port in "${port_list[@]}"; do
            if timeout 3 bash -c "exec 3<>/dev/tcp/$peer_ip/$port" 2>/dev/null; then
                printf 'TCP_PROBE NODE=%s IP=%s PORT=%s STAGE=%s RESULT=OPEN\n' \
                    "$peer" "$peer_ip" "$port" "$stage"
            else
                printf 'TCP_PROBE NODE=%s IP=%s PORT=%s STAGE=%s RESULT=CLOSED_OR_FILTERED\n' \
                    "$peer" "$peer_ip" "$port" "$stage"
                failures=$((failures + 1))
            fi
        done
    done <"$TARGETS"
    printf 'Q200_TCP_PROBE_FAILURE_COUNT=%s\n' "$failures"
    if [[ "$failures" -eq 0 ]]; then
        printf 'Q200_TCP_SERVICE_PATH_OBSERVATION=ALL_LISTED_PORTS_OPEN\n'
    else
        printf 'Q200_TCP_SERVICE_PATH_OBSERVATION=PARTIAL_DEFER_TO_OWNER_TASK_STAGE\n'
    fi
    printf 'Q200_TCP_PROBES_ARE_OBSERVATIONAL_NOT_LATER_TASK_PASS\n'
}

preflight() {
    for c in awk bash date getent grep hostname ip sha256sum stat systemctl tee timedatectl timeout; do need "$c"; done
    lock_identity_gate
    assert_service_boundary
    time_hosts_routes_firewall
    printf 'Q200_NGINX_PREFLIGHT_PASS\n'
    : >"$evidence_dir/preflight.pass"
}

execute() {
    [[ ${Q200_EXECUTION_SCOPE:-} == READ_ONLY_GATE_ONLY ]] ||
        die 'execute is restricted to READ_ONLY_GATE_ONLY'
    [[ -f "$evidence_dir/preflight.pass" ]] || die 'preflight marker is absent'
    lock_identity_gate
    assert_service_boundary
    source_policy_gate
    read_only_tcp_probes
    : >"$evidence_dir/execute.pass"
    printf 'Q200_NGINX_READ_ONLY_EXECUTE_PASS\n'
}

verify() {
    [[ -f "$evidence_dir/execute.pass" ]] || die 'execute marker is absent'
    lock_identity_gate
    assert_service_boundary
    time_hosts_routes_firewall
    read_only_tcp_probes
    printf 'Q200_NGINX_NODE_GATE_PASS\n'
    printf 'Q200_GLOBAL_PASS=NOT_ASSERTED_BY_ROLE_PACKAGE\n'
    : >"$evidence_dir/verify.pass"
}

rollback() {
    lock_identity_only
    printf 'Q200_ROLLBACK=NO_SYSTEM_CHANGE_TO_ROLL_BACK\n'
}

archive() {
    lock_identity_only
    local archive="$evidence_root/${run_id}-${node}-evidence.tar.gz"
    tar -C "$evidence_root" -czf "$archive" "$run_id/$node"
    sha256sum "$archive"
    printf 'Q200_ARCHIVE=%s\n' "$archive"
}

case "$mode" in
    --static-validate) static_validate ;;
    preflight) preflight ;;
    execute) execute ;;
    verify) verify ;;
    rollback) rollback ;;
    archive) archive ;;
esac
