#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")
MODE=${1:-}
NODE=
TARGET_HOST=LOCAL_STATIC_ONLY
TARGET_IP=LOCAL_STATIC_ONLY
FINAL_RESULT=FAIL

usage() {
    printf '%s\n' \
        'Usage:' \
        '  q191-app-resource-network-runtime-readonly.sh --static-validate' \
        '  q191-app-resource-network-runtime-readonly.sh run --node app01|app02'
}

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

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

positive_integer() {
    [[ "$1" =~ ^[1-9][0-9]*$ ]]
}

finish() {
    local rc=$?
    trap - EXIT
    if [[ "$rc" -eq 0 ]]; then
        FINAL_RESULT=PASS
    fi
    printf 'Q191_END_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
    printf 'Q191_MODE=%s\n' "$MODE"
    printf 'Q191_NODE=%s\n' "${NODE:-STATIC}"
    printf 'Q191_TARGET=%s/%s\n' "$TARGET_HOST" "$TARGET_IP"
    printf 'Q191_RC=%s\n' "$rc"
    printf 'Q191_RESULT=%s\n' "$FINAL_RESULT"
    printf 'Q191_DURABLE_STATE_MUTATION=ZERO\n'
    printf 'Q191_ROLLBACK_STATE=NOT_REQUIRED_READONLY\n'
    printf 'Q191_Q111_Q112_ACTION=NOT_PERFORMED\n'
    printf 'Q191_Q200_NETWORK_CHANGE=NOT_PERFORMED\n'
    printf 'Q191_END\n'
    exit "$rc"
}
trap finish EXIT

case "$MODE" in
    --static-validate)
        [[ $# -eq 1 ]] || {
            usage
            exit 2
        }
        ;;
    run)
        [[ $# -eq 3 && ${2:-} == --node ]] || {
            usage
            exit 2
        }
        NODE=$3
        case "$NODE" in
            app01)
                TARGET_HOST=sqjcapp01
                TARGET_IP=10.2.203.104
                ;;
            app02)
                TARGET_HOST=sqjcapp02
                TARGET_IP=10.2.203.105
                ;;
            *)
                die "unsupported node: $NODE"
                exit 2
                ;;
        esac
        ;;
    *)
        usage
        exit 2
        ;;
esac

printf 'Q191_BEGIN\n'
printf 'Q191_START_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'Q191_MODE=%s\n' "$MODE"
printf 'Q191_NODE=%s\n' "${NODE:-STATIC}"
printf 'Q191_TARGET=%s/%s\n' "$TARGET_HOST" "$TARGET_IP"

static_validate() {
    local marker
    bash -n "$SCRIPT_PATH"
    for marker in \
        --static-validate \
        'run --node app01|app02' \
        Q191_DEP_Q160 \
        Q191_DEP_Q190 \
        Q191_EXTERNAL_VM_RESOURCE_CONFIRMATION \
        Q191_APP_LOCK_GRANTED \
        Q191_APP_LOCK_OWNER \
        Q191_MIN_ROOT_FREE_BYTES \
        Q191_DURABLE_STATE_MUTATION=ZERO \
        Q191_Q111_Q112_ACTION=NOT_PERFORMED \
        Q191_Q200_NETWORK_CHANGE=NOT_PERFORMED \
        supervisor.service \
        crond.service \
        DEFER_Q200
    do
        grep -Fq -- "$marker" "$SCRIPT_PATH" || die "missing marker: $marker"
    done
    printf 'Q191_APP_LOCAL_STATIC_VALIDATE=PASS\n'
    printf 'Q191_REMOTE_STATUS=NOT_EXECUTED\n'
}

dependency_lock_gate() {
    [[ ${Q191_DEP_Q160:-} == PASS ]] || die 'Q160_PASS is not asserted'
    [[ ${Q191_DEP_Q190:-} == PASS ]] || die 'Q190_PASS is not asserted'
    [[ ${Q191_EXTERNAL_VM_RESOURCE_CONFIRMATION:-} == PASS ]] ||
        die 'external App VM resource confirmation is absent'
    [[ ${Q191_APP_LOCK_GRANTED:-} == 1 ]] || die 'APP_Q191 lock is not granted'
    [[ ${Q191_APP_LOCK_OWNER:-} == APP_Q191 ]] || die 'APP_Q191 lock owner mismatch'
    positive_integer "${Q191_MIN_ROOT_FREE_BYTES:-}" ||
        die 'Q191_MIN_ROOT_FREE_BYTES is missing or invalid'
    [[ ${Q191_READONLY_ONLY:-} == 1 ]] || die 'read-only execution boundary is not asserted'
    printf 'Q191_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 -v expected="$TARGET_IP" '
            {split($4, address, "/"); if (address[1] == expected) ok=1}
            END {exit !ok}
        ' || die 'target IP mismatch'
    [[ -r /etc/zhct/node.env ]] || die 'node.env is absent'
    grep -Fxq 'ROLE=app' /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 'Q191_IDENTITY_GATE=PASS\n'
}

resource_gate() {
    local vcpu mem_bytes max_disk_bytes root_bytes root_free
    vcpu=$(nproc)
    mem_bytes=$(awk '/^MemTotal:/ {printf "%.0f\n", $2 * 1024}' /proc/meminfo)
    max_disk_bytes=$(lsblk -bdno TYPE,SIZE |
        awk '$1=="disk" && $2>max {max=$2} END {printf "%.0f\n", max+0}')
    root_bytes=$(df -B1 --output=size / | awk 'NR==2 {print $1}')
    root_free=$(df -B1 --output=avail / | awk 'NR==2 {print $1}')
    printf 'RESOURCE_VCPU=%s EXPECTED=4\n' "$vcpu"
    printf 'RESOURCE_MEM_BYTES=%s EXPECTED_NOMINAL_GIB=8\n' "$mem_bytes"
    printf 'RESOURCE_MAX_DISK_BYTES=%s EXPECTED_NOMINAL_GB=50\n' "$max_disk_bytes"
    printf 'RESOURCE_ROOT_FS_BYTES=%s EXPECTED_MIN_BYTES=40000000000\n' "$root_bytes"
    printf 'RESOURCE_ROOT_FREE_BYTES=%s APPROVED_MIN_BYTES=%s\n' \
        "$root_free" "$Q191_MIN_ROOT_FREE_BYTES"
    [[ "$vcpu" -eq 4 ]] || die 'vCPU count differs from approved 4C target'
    [[ "$mem_bytes" -ge 7516192768 ]] ||
        die 'memory is below the 7 GiB OS-visible floor for 8G target'
    [[ "$max_disk_bytes" -ge 49000000000 ]] ||
        die 'system disk is below the 50 GB nominal target floor'
    [[ "$root_bytes" -ge 40000000000 ]] ||
        die 'root filesystem is below the accepted approximately 44G layout floor'
    [[ "$root_free" -ge "$Q191_MIN_ROOT_FREE_BYTES" ]] ||
        die 'root free space is below the approved App PHP/log/release budget'
    printf 'Q191_APP_RESOURCE_CAPACITY_GATE=PASS\n'
}

resource_snapshot() {
    printf 'Q191_LSCPU_BEGIN\n'
    lscpu
    printf 'Q191_LSCPU_END\n'
    free -b
    lsblk -b -o NAME,TYPE,SIZE,FSTYPE,MOUNTPOINTS
    findmnt -R /
    df -B1 -T / /var/log /var/backups /workspace
    if command -v pvs >/dev/null 2>&1; then pvs --units b --nosuffix; fi
    if command -v vgs >/dev/null 2>&1; then vgs --units b --nosuffix; fi
    if command -v lvs >/dev/null 2>&1; then lvs --units b --nosuffix; fi
    printf 'Q191_APP_RESOURCE_SNAPSHOT=PASS\n'
}

time_gate() {
    timedatectl show -p Timezone -p NTP -p NTPSynchronized
    [[ "$(timedatectl show -p NTPSynchronized --value)" == yes ]] ||
        die 'NTP is not synchronized'
    if command -v chronyc >/dev/null 2>&1; then
        chronyc tracking
        chronyc sources -v
    fi
    printf 'Q191_TIME_READONLY_GATE=PASS\n'
}

name_resolution_snapshot() {
    local host ip_value result deferred=0
    printf 'Q191_HOSTS_FILE_BEGIN\n'
    sed -n '1,240p' /etc/hosts
    printf 'Q191_HOSTS_FILE_END\n'
    printf 'Q191_RESOLV_CONF_BEGIN\n'
    sed -n '1,160p' /etc/resolv.conf
    printf 'Q191_RESOLV_CONF_END\n'
    while read -r host ip_value; do
        if getent ahostsv4 "$host" |
            awk -v expected="$ip_value" '$1==expected {ok=1} END {exit !ok}'
        then
            result=MATCH
        else
            result=MISSING_OR_MISMATCH_DEFER_Q200
            deferred=$((deferred + 1))
        fi
        printf 'Q191_NAME_RESOLUTION HOST=%s IP=%s RESULT=%s\n' \
            "$host" "$ip_value" "$result"
    done <<'HOSTS'
sqjcnginx01 10.2.203.102
sqjcnginx02 10.2.203.103
sqjcapp01 10.2.203.104
sqjcapp02 10.2.203.105
sqjcDB01 10.2.208.106
sqjcDB02 10.2.208.107
sqjcmqtt01 10.2.203.108
sqjcmqtt02 10.2.203.109
sqjcmqtt03 10.2.203.110
sqjcredis01 10.2.203.112
sqjcredis02 10.2.203.113
front-vip 10.2.203.115
mqtt-vip 10.2.203.116
db-proxy-vip 10.2.203.117
HOSTS
    printf 'Q191_NAME_RESOLUTION_DEFER_Q200_COUNT=%s\n' "$deferred"
    printf 'Q191_HOSTS_DNS_SNAPSHOT=PASS_NOT_Q200_CONFIGURATION_PASS\n'
}

route_firewall_snapshot() {
    local destination route_rc
    ip -o -4 addr show
    ip route show table all
    for destination in \
        10.2.203.102 10.2.203.103 10.2.203.104 10.2.203.105 \
        10.2.208.106 10.2.208.107 \
        10.2.203.108 10.2.203.109 10.2.203.110 \
        10.2.203.112 10.2.203.113 \
        10.2.203.115 10.2.203.116 10.2.203.117
    do
        set +e
        ip route get "$destination"
        route_rc=$?
        set -e
        printf 'Q191_ROUTE_PROBE DESTINATION=%s RC=%s JUDGMENT=%s\n' \
            "$destination" "$route_rc" \
            "$([[ "$route_rc" -eq 0 ]] && echo PRESENT || echo MISSING_DEFER_Q200)"
    done
    if command -v firewall-cmd >/dev/null 2>&1; then
        firewall-cmd --state || true
        firewall-cmd --get-active-zones || true
        firewall-cmd --list-all-zones || true
    else
        printf 'Q191_FIREWALL_CMD=ABSENT_DEFER_Q200\n'
    fi
    printf 'Q191_ROUTE_FIREWALL_SNAPSHOT=PASS_NOT_Q200_CONFIGURATION_PASS\n'
}

runtime_gate() {
    local unit enabled active
    for unit in nginx.service php-fpm73.service php-fpm74.service; do
        enabled=$(systemctl is-enabled "$unit" 2>/dev/null || true)
        active=$(systemctl is-active "$unit" 2>/dev/null || true)
        printf 'Q191_UNIT=%s ENABLED=%s ACTIVE=%s EXPECTED=enabled/active\n' \
            "$unit" "$enabled" "$active"
        [[ "$enabled" == enabled && "$active" == active ]] ||
            die "required App runtime unit mismatch: $unit"
    done
    for unit in supervisor.service crond.service; do
        enabled=$(systemctl is-enabled "$unit" 2>/dev/null || true)
        active=$(systemctl is-active "$unit" 2>/dev/null || true)
        printf 'Q191_UNIT=%s ENABLED=%s ACTIVE=%s EXPECTED=disabled/inactive\n' \
            "$unit" "$enabled" "$active"
        [[ "$enabled" == disabled && "$active" == inactive ]] ||
            die "forbidden pre-Q263 unit mismatch: $unit"
    done
    pgrep -x nginx >/dev/null || die 'nginx process is absent'
    pgrep -x php-fpm >/dev/null || die 'php-fpm process is absent'
    ! pgrep -x supervisord >/dev/null || die 'supervisord exists before Q263'
    ss -lntup
    printf 'Q191_APP_RUNTIME_READONLY_GATE=PASS\n'
    printf 'Q191_SUPERVISOR_CROND_WORKERS_SINGLETONS=DISABLED_INACTIVE_NOT_STARTED\n'
}

run_gate() {
    dependency_lock_gate
    for command in awk bash cut date df findmnt free getent grep hostname id ip \
        lscpu lsblk nproc pgrep sed ss stat systemctl timedatectl
    do
        need "$command"
    done
    identity_gate
    resource_gate
    resource_snapshot
    time_gate
    name_resolution_snapshot
    route_firewall_snapshot
    runtime_gate
    printf 'Q191_APP_NODE_READONLY_PASS\n'
    printf 'Q191_BOTH_APP_NODES_PASS=NOT_ASSERTED_BY_SINGLE_NODE_RUN\n'
    printf 'Q191_Q200_PASS=NOT_ASSERTED\n'
}

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