#!/usr/bin/env bash

set -Eeuo pipefail
umask 077
export LANG=C

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:' \
        '  q160-app-role-readonly.sh --static-validate' \
        '  q160-app-role-readonly.sh run --node app01|app02'
}

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

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

finish() {
    local rc=$?
    trap - EXIT
    if [[ "$rc" -eq 0 ]]; then
        FINAL_RESULT=PASS
    fi
    printf 'Q160_APP_END_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
    printf 'Q160_APP_MODE=%s\n' "$MODE"
    printf 'Q160_APP_NODE=%s\n' "${NODE:-STATIC}"
    printf 'Q160_APP_TARGET=%s/%s\n' "$TARGET_HOST" "$TARGET_IP"
    printf 'Q160_APP_RC=%s\n' "$rc"
    printf 'Q160_APP_RESULT=%s\n' "$FINAL_RESULT"
    printf 'Q160_APP_DURABLE_STATE_MUTATION=ZERO\n'
    printf 'Q160_APP_ROLLBACK_STATE=NOT_REQUIRED_READONLY\n'
    printf 'Q160_APP_Q110=PASS_RETAINED\n'
    printf 'Q160_APP_Q111=WAITING_MANUAL_EXECUTION_NOT_PERFORMED\n'
    printf 'Q160_APP_Q112=NOT_COMPLETED_NOT_PERFORMED\n'
    printf 'Q160_APP_Q191_Q200_Q260_PLUS=NOT_PERFORMED\n'
    printf 'Q160_APP_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" ;;
        esac
        ;;
    *) usage; exit 2 ;;
esac

printf 'Q160_APP_BEGIN\n'
printf 'Q160_APP_START_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'Q160_APP_MODE=%s\n' "$MODE"
printf 'Q160_APP_NODE=%s\n' "${NODE:-STATIC}"
printf 'Q160_APP_TARGET=%s/%s\n' "$TARGET_HOST" "$TARGET_IP"
printf 'Q160_DEP_Q142=PASS evidence=q142-stage2-remote-output-20260805T151115+0800.txt sha256=b05660da635652b88d00e56589f0b735d3d8267688f5418be95d44368884331e\n'
printf 'Q160_DEP_Q143=PASS evidence=q143-stage2-remote-output-20260805T173257+0800.txt sha256=cdc01a49031a8a501cab6e2d6eb11692e2e8985544c62b2602e361c064cc0dc1\n'
printf 'Q160_APP_SCOPE=READONLY_CROSS_ACCEPTANCE_NOT_ACTIVATION\n'

static_validate() {
    local marker
    bash -n "$SCRIPT_PATH"
    for marker in \
        --static-validate \
        'run --node app01|app02' \
        Q160_DEP_Q142=PASS \
        Q160_DEP_Q143=PASS \
        Q160_APP_Q110=PASS_RETAINED \
        Q160_APP_Q111=WAITING_MANUAL_EXECUTION_NOT_PERFORMED \
        Q160_APP_Q112=NOT_COMPLETED_NOT_PERFORMED \
        Q160_APP_DURABLE_STATE_MUTATION=ZERO \
        supervisor.service \
        crond.service \
        Q160_APP_Q191_Q200_Q260_PLUS=NOT_PERFORMED
    do
        grep -Fq -- "$marker" "$SCRIPT_PATH" || die "missing marker: $marker"
    done
    printf 'Q160_APP_LOCAL_STATIC_VALIDATE=PASS\n'
    printf 'Q160_APP_REMOTE_STATUS=NOT_EXECUTED\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'
    if [[ "$NODE" == app01 ]]; then
        [[ ! -e /etc/zhct/node.env && ! -L /etc/zhct/node.env ]] ||
            die 'app01 node.env unexpectedly exists before Q111'
        printf 'Q160_APP_NODE_ENV_STATE=ABSENT_EXPECTED_Q053_SEALED_PRE_Q111\n'
        printf 'Q160_APP_NODE_ENV_IDENTITY=DEFERRED_TO_Q111_NOT_ACTIVATED\n'
    else
        [[ -f /etc/zhct/node.env && ! -L /etc/zhct/node.env ]] ||
            die 'app02 node.env is absent or unsafe after Q110 PASS'
        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'
        grep -Fxq "NODE_NAME=$TARGET_HOST" /etc/zhct/node.env || die 'node name mismatch'
        printf 'Q160_APP_NODE_ENV_STATE=PRESENT_VALID_Q110_POSTCLONE\n'
        printf 'Q160_APP_NODE_ENV_SAFE_BEGIN\n'
        grep -E '^(ROLE|NODE_KEY|NODE_IP|NODE_NAME|APPLIED_AT)=' /etc/zhct/node.env
        printf 'Q160_APP_NODE_ENV_SAFE_END\n'
    fi
    printf 'Q160_APP_IDENTITY_GATE=PASS\n'
}

template_and_role_asset_gate() {
    local expected relative actual
    while read -r expected relative; do
        [[ -f "/opt/zhct-deploy/$relative" && ! -L "/opt/zhct-deploy/$relative" ]] ||
            die "missing or unsafe asset: $relative"
        actual=$(sha256sum "/opt/zhct-deploy/$relative" | awk '{print $1}')
        printf 'Q160_APP_ROLE_ASSET=%s EXPECTED_SHA256=%s ACTUAL_SHA256=%s\n' \
            "$relative" "$expected" "$actual"
        [[ "$actual" == "$expected" ]] || die "asset SHA mismatch: $relative"
    done <<'ASSETS'
ca5fa0a113739bcbe3ff51b2815bb50723cd8b1cf33dfb6d3f3f172caf2dabc6 common/scripts/post-clone.sh
e637ca657a897c6d0b689bbd6347f04516baf5c9f32350cd1e8a2071c211a0a5 common/scripts/activate-role.sh
e3eae16aa5c87f43b56495fc71de378a947578cabe18b507e8fed01ac1f32025 common/bin/role-action.sh
03c6b13a0def5bf3954e069581b6f9fbe0f81f24285ba2319b6279c0406d70aa roles/app/hooks.sh
ef700d329e2767e1ee1c9733fb26e1d5e256bab3ed023bceb9e5d01a7f5df46f roles/app/role.env
83e6abb26b4af6d8a8dc74082acac73009d0d60446bd641f65b8b4e9c21e6afc roles/app/manifests/nodes.tsv
d43e179c108f6db551b452256f867d6d4f6fc9e031c1d4def2f343966af8d684 roles/app/manifests/systemd-units.tsv
d4e34f3261e0068a97acc101600c1d096458be4b5146483e42672f59aa7c433c roles/app/manifests/ports.tsv
ASSETS
    for relative in .source-commit .framework-bundle-sha256; do
        [[ -f "/opt/zhct-deploy/$relative" && ! -L "/opt/zhct-deploy/$relative" ]] ||
            die "template provenance marker missing or unsafe: $relative"
        stat -c 'Q160_APP_TEMPLATE_MARKER path=%n mode=%a owner=%U:%G size=%s mtime=%y' \
            "/opt/zhct-deploy/$relative"
        sha256sum "/opt/zhct-deploy/$relative"
    done
    printf 'Q160_APP_TEMPLATE_ROLE_ASSET_GATE=PASS\n'
}

unique_identity_evidence() {
    local machine_id key_count
    [[ -f /etc/machine-id && ! -L /etc/machine-id ]] || die 'machine-id missing or unsafe'
    if [[ "$NODE" == app01 ]]; then
        [[ ! -s /etc/machine-id ]] || die 'app01 sealed machine-id unexpectedly populated before Q111'
        [[ ! -e /var/lib/dbus/machine-id && ! -L /var/lib/dbus/machine-id ]] ||
            die 'app01 sealed DBus machine-id unexpectedly exists before Q111'
        key_count=$(find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*_key*' -print | wc -l)
        [[ "$key_count" -eq 0 ]] || die 'app01 sealed SSH host key files unexpectedly exist before Q111'
        printf 'Q160_APP_MACHINE_ID_STATE=EMPTY_EXPECTED_Q053_SEALED_PRE_Q111\n'
        printf 'Q160_APP_DBUS_MACHINE_ID_STATE=ABSENT_EXPECTED_Q053_SEALED_PRE_Q111\n'
        printf 'Q160_APP_SSH_HOST_KEY_FILE_COUNT=0\n'
        printf 'Q160_APP_UNIQUE_IDENTITY_STATE=REMOVED_EXPECTED_DEFERRED_TO_Q111\n'
        return
    fi
    machine_id=$(tr -d '\r\n' </etc/machine-id)
    [[ "$machine_id" =~ ^[[:xdigit:]]{32}$ ]] || die 'machine-id format mismatch'
    printf 'Q160_APP_MACHINE_ID_FORMAT=PASS_32_HEX_OPTIONAL_TRAILING_NEWLINE\n'
    printf 'Q160_APP_MACHINE_ID_SHA256='
    sha256sum /etc/machine-id | awk '{print $1}'
    printf 'Q160_APP_SSH_PUBLIC_FINGERPRINTS_BEGIN\n'
    local count=0 public_key
    while IFS= read -r public_key; do
        ssh-keygen -lf "$public_key"
        count=$((count + 1))
    done < <(find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*.pub' -print | sort)
    [[ "$count" -ge 3 ]] || die 'SSH host public keys are incomplete'
    printf 'Q160_APP_SSH_PUBLIC_FINGERPRINTS_END\n'
    printf 'Q160_APP_CROSS_NODE_UNIQUENESS=REQUIRES_BOTH_NODE_OUTPUTS\n'
}

runtime_asset_gate() {
    local required_path
    for required_path in \
        /usr/local/nginx/sbin/nginx \
        /usr/local/php7.3/bin/php \
        /usr/local/php7.3/sbin/php-fpm \
        /usr/local/php7.4/bin/php \
        /usr/local/php7.4/sbin/php-fpm \
        /usr/local/bin/supervisord \
        /usr/local/sbin/singleton-state-check.sh \
        /etc/supervisor/conf.d/sanquan.conf \
        /etc/systemd/system/nginx.service \
        /etc/systemd/system/php-fpm73.service \
        /etc/systemd/system/php-fpm74.service \
        /etc/systemd/system/supervisor.service
    do
        [[ -e "$required_path" && ! -L "$required_path" ]] ||
            die "required runtime asset missing or unsafe: $required_path"
        stat -c 'Q160_APP_RUNTIME_ASSET path=%n type=%F mode=%a owner=%U:%G size=%s' \
            "$required_path"
    done
    if [[ "$NODE" == app01 ]]; then
        [[ ! -e /etc/cron.d/zhct-sanquan && ! -L /etc/cron.d/zhct-sanquan ]] ||
            die 'app01 cron unexpectedly exists before Q111'
        [[ $(awk -F= '/^numprocs=/{sum+=$2} END{print sum+0}' \
            /etc/supervisor/conf.d/sanquan.conf) -eq 9 ]] ||
            die 'app01 supervisor process count mismatch'
        grep -Fq 'dev1_consume_work_%(process_num)d' \
            /etc/supervisor/conf.d/sanquan.conf ||
            die 'app01 supervisor identity mismatch'
        printf 'Q160_APP_CRON_STATE=ABSENT_EXPECTED_Q053_SEALED_PRE_Q111\n'
        printf 'Q160_APP_SINGLETON_POLICY=PARTIAL_TEMPLATE_ONLY_CRON_DEFERRED_TO_Q111\n'
        sha256sum /etc/supervisor/conf.d/sanquan.conf
    else
        [[ -f /etc/cron.d/zhct-sanquan && ! -L /etc/cron.d/zhct-sanquan ]] ||
            die 'app02 cron missing or unsafe after Q110 PASS'
        stat -c 'Q160_APP_RUNTIME_ASSET path=%n type=%F mode=%a owner=%U:%G size=%s' \
            /etc/cron.d/zhct-sanquan
        /usr/local/sbin/singleton-state-check.sh \
            --node "$NODE" \
            --config /etc/supervisor/conf.d/sanquan.conf \
            --cron /etc/cron.d/zhct-sanquan
        printf 'Q160_APP_SINGLETON_POLICY=PASS_CONFIGURED_BUT_NOT_STARTED\n'
        sha256sum /etc/supervisor/conf.d/sanquan.conf /etc/cron.d/zhct-sanquan
    fi
    /usr/local/php7.3/bin/php -r \
        'printf("PHP73=%s redis=%s mosquitto=%s\\n", PHP_VERSION, extension_loaded("redis")?"YES":"NO", extension_loaded("mosquitto")?"YES":"NO");'
    /usr/local/php7.4/bin/php -r \
        'printf("PHP74=%s redis=%s mosquitto=%s\\n", PHP_VERSION, extension_loaded("redis")?"YES":"NO", extension_loaded("mosquitto")?"YES":"NO");'
    /usr/local/bin/supervisord --version
    printf 'Q160_APP_RUNTIME_ASSET_GATE=PASS\n'
}

quiescent_boundary_gate() {
    local unit enabled active substate process_name
    for unit in nginx.service php-fpm73.service php-fpm74.service supervisor.service crond.service; do
        enabled=$(systemctl is-enabled "$unit" 2>/dev/null || true)
        active=$(systemctl is-active "$unit" 2>/dev/null || true)
        substate=$(systemctl show "$unit" -p SubState --value 2>/dev/null || true)
        printf 'Q160_APP_UNIT=%s ENABLED=%s ACTIVE=%s SUBSTATE=%s EXPECTED=disabled/inactive/dead\n' \
            "$unit" "$enabled" "$active" "$substate"
        [[ "$enabled" == disabled && "$active" == inactive && "$substate" == dead ]] ||
            die "unit is not quiescent before Q111/Q112 completion: $unit"
    done
    for process_name in nginx php-fpm supervisord crond php mysqld mysqld_safe redis-server mosquitto; do
        if pgrep -a -x "$process_name"; then
            die "forbidden process exists in current App staging state: $process_name"
        fi
    done
    printf 'Q160_APP_TARGET_PROCESSES=ZERO\n'
    if ss -H -lntup | grep -Eq ':(8081|18081)([[:space:]]|$)'; then
        ss -lntup | grep -E ':(8081|18081)([[:space:]]|$)' || true
        die 'App target listener exists before Q111/Q112 activation'
    fi
    [[ ! -S /run/php-fpm73/php-fpm.sock ]] || die 'php-fpm73 socket exists unexpectedly'
    [[ ! -S /run/php-fpm74/php-fpm.sock ]] || die 'php-fpm74 socket exists unexpectedly'
    printf 'Q160_APP_TARGET_LISTENERS_AND_SOCKETS=ZERO\n'
    printf 'Q160_APP_SUPERVISOR_CROND_WORKERS_SINGLETONS=DISABLED_INACTIVE_NOT_STARTED\n'
}

application_release_boundary() {
    local app_root
    for app_root in \
        /workspace/wwwroot/zhct_sanquan \
        /workspace/wwwroot/aizhct_sanquan \
        /workspace/wwwroot/ai_store_sanquan
    do
        if [[ -e "$app_root" || -L "$app_root" ]]; then
            stat -c 'Q160_APP_RELEASE_PATH_PRESENT path=%n type=%F mode=%a owner=%U:%G size=%s' "$app_root"
            die "formal application root exists before Q260+: $app_root"
        fi
        printf 'Q160_APP_RELEASE_PATH_ABSENT=%s\n' "$app_root"
    done
    printf 'Q160_APP_RELEASE_BOUNDARY=PASS_Q260_NOT_PERFORMED\n'
}

run_gate() {
    local command
    for command in awk bash date find grep hostname id ip pgrep sha256sum ssh-keygen ss stat systemctl wc; do
        need "$command"
    done
    identity_gate
    template_and_role_asset_gate
    unique_identity_evidence
    runtime_asset_gate
    quiescent_boundary_gate
    application_release_boundary
    printf 'Q160_APP_NODE_READONLY_PASS_STAGE_AWARE\n'
    if [[ "$NODE" == app01 ]]; then
        printf 'Q160_APP_ACTIVATION_READINESS=BLOCKED_PENDING_Q111_POSTCLONE_IDENTITY_AND_CRON\n'
    else
        printf 'Q160_APP_ACTIVATION_READINESS=BLOCKED_PENDING_Q112_ACTIVATION\n'
    fi
    printf 'Q160_APP_GLOBAL_PASS=REQUIRES_BOTH_NODE_OUTPUTS_AND_LOCAL_CROSS_COMPARE\n'
}

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