#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")
Q143_RECEIPT_SHA256=cdc01a49031a8a501cab6e2d6eb11692e2e8985544c62b2602e361c064cc0dc1

normalized_stream_sha() {
    awk '!/^[[:space:]]*mysqlx[[:space:]]*=[[:space:]]*OFF[[:space:]]*$/' |
        sha256sum | awk '{print $1}'
}

normalized_config_sha() {
    normalized_stream_sha <"$1"
}

usage() {
    cat <<'USAGE'
Usage:
  q160-mysql-role-readonly.sh --static-validate
  q160-mysql-role-readonly.sh --self-test-contract
  q160-mysql-role-readonly.sh run
USAGE
}

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

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

mode=${1:-}
case "$mode" in
    --static-validate|--self-test-contract)
        [[ $# -eq 1 ]] || { usage; exit 2; }
        node=STATIC
        target_host=LOCAL_STATIC_ONLY
        target_ip=LOCAL_STATIC_ONLY
        expected_profile=LOCAL_STATIC_ONLY
        expected56_id=0
        expected80_id=0
        expected_read_only=0
        receipt_path=LOCAL_STATIC_ONLY
        receipt_sha256=LOCAL_STATIC_ONLY
        ;;
    run)
        [[ $# -eq 1 ]] || { usage; exit 2; }
        case "$(hostname -s)" in
            sqjcDB01)
                node=mysql01
                target_host=sqjcDB01
                target_ip=10.2.208.106
                expected_profile=mysql-primary-candidate
                expected_clone_source=template
                expected56_id=56106
                expected80_id=80106
                expected_read_only=0
                receipt_path=/root/sanquan-mysql-q121-evidence-q121-20260729T185007+0800.tar.gz
                receipt_sha256=e1b48bace6b73697278d0313a6c27e8be7c2e3fce231cfa8f2e8f6548261a116
                ;;
            sqjcDB02)
                node=mysql02
                target_host=sqjcDB02
                target_ip=10.2.208.107
                expected_profile=mysql-secondary-initial-read-only
                expected_clone_source=mysql01
                expected56_id=56107
                expected80_id=80107
                expected_read_only=1
                receipt_path=/root/sanquan-mysql-q122-evidence-q122-20260729T190725+0800.tar.gz
                receipt_sha256=d20143cc9df939c33de3aa1b8e08ebd1b4d59b6a4322b32d68d83dc622b69808
                ;;
            *)
                die "unsupported hostname: $(hostname -s)"
                exit 1
                ;;
        esac
        ;;
    *)
        usage
        exit 2
        ;;
esac

run_id=${Q160_RUN_ID:-q160-mysql-$(date '+%Y%m%dT%H%M%S%z')}
[[ "$run_id" =~ ^q160-mysql-[A-Za-z0-9._+-]+$ ]] || die 'invalid Q160_RUN_ID'

finish() {
    local rc=$?
    printf 'Q160_MYSQL_END=%s\n' "$mode"
    printf 'finished_at=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
    printf 'target_hostname=%s\n' "$target_host"
    printf 'target_ip=%s\n' "$target_ip"
    printf 'target_node=%s\n' "$node"
    printf 'Q160_MYSQL_RC=%s\n' "$rc"
    printf 'Q160_MYSQL_ROLLBACK_STATE=NO_SYSTEM_CHANGE_TO_ROLL_BACK\n'
    printf 'Q192_RESOURCE_ACCEPTANCE=NOT_PERFORMED\n'
    printf 'Q200_NETWORK_GATE=NOT_PERFORMED\n'
    printf 'Q221_REPLICATION=NOT_PERFORMED\n'
    printf 'Q261_BUSINESS_SQL=NOT_PERFORMED\n'
    if [[ "$rc" -eq 0 && "$mode" == run ]]; then
        printf 'Q160_MYSQL_JUDGMENT=NODE_READONLY_PASS_NOT_GLOBAL_Q160_PASS\n'
    elif [[ "$rc" -eq 0 ]]; then
        printf 'Q160_MYSQL_JUDGMENT=LOCAL_STATIC_ONLY_REMOTE_NOT_EXECUTED\n'
    else
        printf 'Q160_MYSQL_JUDGMENT=FAIL_CLOSED\n'
    fi
    return "$rc"
}
trap finish EXIT

printf 'Q160_MYSQL_BEGIN=%s\n' "$mode"
printf 'started_at=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'target_hostname=%s\n' "$target_host"
printf 'target_ip=%s\n' "$target_ip"
printf 'target_node=%s\n' "$node"
printf 'run_id=%s\n' "$run_id"
printf 'Q160_DEP_Q143_RECEIPT_SHA256=%s\n' "$Q143_RECEIPT_SHA256"
printf 'Q160_MYSQL_ENTRY_VERSION=V2_MYSQLX_APPROVED_DELTA\n'

static_validate() {
    bash -n "$SCRIPT_PATH"
    local marker
    for marker in \
        Q160_DEP_Q143_RECEIPT_SHA256 Q160_MYSQL_ENTRY_VERSION=V2_MYSQLX_APPROVED_DELTA \
        MYSQLX_APPROVED_DELTA=PASS_ONLY_MYSQLX_OFF Q160_MYSQL_NODE_READONLY_PASS \
        Q192_RESOURCE_ACCEPTANCE=NOT_PERFORMED Q200_NETWORK_GATE=NOT_PERFORMED \
        Q221_REPLICATION=NOT_PERFORMED Q261_BUSINESS_SQL=NOT_PERFORMED; do
        grep -Fq -- "$marker" "$SCRIPT_PATH" || die "missing contract marker: $marker"
    done
    printf 'Q160_MYSQL_LOCAL_STATIC_VALIDATE_PASS\n'
    printf 'Q160_MYSQL_REMOTE_STATUS=NOT_EXECUTED\n'
}

identity_contract() {
    local actual_host=$1 expected_host=$2 actual_ip=$3 expected_ip=$4
    [[ "$actual_host" == "$expected_host" ]] || return 1
    [[ "$actual_ip" == "$expected_ip" ]] || return 1
}

self_test_contract() {
    identity_contract sqjcDB01 sqjcDB01 10.2.208.106 10.2.208.106 ||
        die 'mysql01 positive identity contract failed'
    identity_contract sqjcDB02 sqjcDB02 10.2.208.107 10.2.208.107 ||
        die 'mysql02 positive identity contract failed'
    if identity_contract wrong-host sqjcDB01 10.2.208.106 10.2.208.106; then
        die 'wrong hostname was accepted'
    fi
    if identity_contract sqjcDB02 sqjcDB02 10.2.208.106 10.2.208.107; then
        die 'wrong IP was accepted'
    fi
    local normalized_base normalized_delta normalized_drift
    normalized_base=$(printf '[mysqld]\nport=3308\n' | normalized_stream_sha)
    normalized_delta=$(printf '[mysqld]\nmysqlx=OFF\nport=3308\n' | normalized_stream_sha)
    normalized_drift=$(printf '[mysqld]\nmysqlx=OFF\nport=3309\n' | normalized_stream_sha)
    [[ "$normalized_base" == "$normalized_delta" ]] || die 'approved mysqlx delta was rejected'
    [[ "$normalized_base" != "$normalized_drift" ]] || die 'non-mysqlx drift was accepted'
    printf 'Q160_MYSQL_IDENTITY_CONTRACT_SELF_TEST_PASS\n'
    printf 'Q160_MYSQL_MYSQLX_DELTA_CONTRACT_SELF_TEST_PASS\n'
}

identity_gate() {
    local actual_ip
    [[ $(id -u) -eq 0 ]] || die 'root is required for metadata inspection'
    actual_ip=$(ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1 |
        grep -Fx "$target_ip" | head -n 1)
    identity_contract "$(hostname -s)" "$target_host" "$actual_ip" "$target_ip" ||
        die 'hostname or IP identity mismatch'
    [[ -r /etc/zhct/node.env ]] || die 'node.env is absent or unreadable'
    grep -Fxq 'ROLE=mysql' /etc/zhct/node.env || die 'node.env role mismatch'
    grep -Fxq "NODE_KEY=$node" /etc/zhct/node.env || die 'node.env node key mismatch'
    grep -Fxq "NODE_IP=$target_ip" /etc/zhct/node.env || die 'node.env IP mismatch'
    grep -Fxq "NODE_NAME=$target_host" /etc/zhct/node.env || die 'node.env hostname mismatch'
    printf 'Q160_MYSQL_IDENTITY_GATE=PASS\n'
    printf 'NODE_ENV_SAFE_FIELDS_BEGIN\n'
    grep -E '^(ROLE|NODE_KEY|NODE_IP|NODE_NAME|APPLIED_AT)=' /etc/zhct/node.env
    printf 'NODE_ENV_SAFE_FIELDS_END\n'
}

receipt_gate() {
    local actual
    [[ -f "$receipt_path" && ! -L "$receipt_path" ]] || die 'Q-121/Q-122 evidence receipt is absent or unsafe'
    actual=$(sha256sum "$receipt_path" | awk '{print $1}')
    printf 'Q121_Q122_NODE_RECEIPT=%s\n' "$receipt_path"
    printf 'Q121_Q122_NODE_RECEIPT_SHA256=%s\n' "$actual"
    [[ "$actual" == "$receipt_sha256" ]] || die 'Q-121/Q-122 evidence receipt digest mismatch'
    printf 'Q160_MYSQL_Q121_Q122_RECEIPT_GATE=PASS\n'
}

role_asset_gate() {
    local base=/opt/zhct-deploy/roles/mysql
    local expected relative actual base80 staged80 live80
    local base_mysqlx_count staged_mysqlx_count live_mysqlx_count
    local base_normalized staged_normalized live_normalized
    while read -r expected relative; do
        [[ -r "$base/$relative" && ! -L "$base/$relative" ]] || die "missing or unsafe 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'
8dd8ad31318a6111fdaf9e0d346dad12fa5da5e629b2dfd429a2e66a3e5e016a hooks.sh
20f7c95907d4ba9ed393de854a47c424162cc3a1a0e6db04abba5cbcfef0ec99 role.env
ba1048576a589845a643158da9fbe18cc21c17ab65d7fcc3dfb2c1797e50e8b1 manifests/nodes.tsv
afd87603242d11b96fecfd44bc1c8bb6edf593e921e735e92277180ae5b80070 manifests/ports.tsv
157a7b2f62b40a163d61d367698519d7a919465d6c650c082582101afecf6ba0 manifests/systemd-units.tsv
e6a7a520b5a2483f623921af98526893f325896f00eae4baca5c362c60060c4b templates/systemd/mysql56.service
83ebe1813a4e61054078e5a0b40c5f16de8f46c0b1fcfa0110aacede193bea8c templates/systemd/mysql80.service
72a3b4291663a81f2c4153f98c74511043a51095b895587ac663a0a04a28728c templates/systemd/mysql-health56.service
e6f432b7d064fda7c65fe881ba69eebe0e38b0b7f3a0d4f0c9bb5faa5018eee5 templates/systemd/mysql-health80.service
ASSETS
    if [[ "$node" == mysql01 ]]; then
        [[ $(sha256sum "$base/templates/nodes/mysql01/my56.cnf" | awk '{print $1}') == 064c848a6bab8c0fee1981ec4838c1eabaa6c5842d8653b653a4ba13fb2c1677 ]] || die 'mysql01 my56 template digest mismatch'
        [[ $(sha256sum "$base/templates/nodes/mysql01/my80.cnf" | awk '{print $1}') == 318384660f42fddb891e9218fdf4674996d5d8ee4232bcc3f6c7b76b364580cf ]] || die 'mysql01 my80 template digest mismatch'
    else
        [[ $(sha256sum "$base/templates/nodes/mysql02/my56.cnf" | awk '{print $1}') == 7a1279d5d22d4d09a4f5631c943aef29aae26e3889ee2c650e28db9614b73ce1 ]] || die 'mysql02 my56 template digest mismatch'
        [[ $(sha256sum "$base/templates/nodes/mysql02/my80.cnf" | awk '{print $1}') == ccb26914288bac4ce4dc8d90c1198f6c16695a847ca9b8ec6e907803ffc335d9 ]] || die 'mysql02 my80 template digest mismatch'
    fi
    cmp -s "$base/templates/nodes/$node/my56.cnf" /etc/my56.cnf || die 'active my56.cnf differs from locked node template'
    base80="$base/templates/nodes/$node/my80.cnf"
    staged80="/etc/zhct/templates/mysql/nodes/$node/my80.cnf"
    live80=/etc/my80.cnf
    [[ -f "$staged80" && ! -L "$staged80" && -f "$live80" && ! -L "$live80" ]] ||
        die 'staged or live my80.cnf is missing or unsafe'
    cmp -s "$staged80" "$live80" || die 'staged and live my80.cnf differ'
    base_mysqlx_count=$(grep -Ec '^[[:space:]]*mysqlx[[:space:]]*=' "$base80" || true)
    staged_mysqlx_count=$(grep -Ec '^[[:space:]]*mysqlx[[:space:]]*=[[:space:]]*OFF[[:space:]]*$' "$staged80" || true)
    live_mysqlx_count=$(grep -Ec '^[[:space:]]*mysqlx[[:space:]]*=[[:space:]]*OFF[[:space:]]*$' "$live80" || true)
    [[ "$base_mysqlx_count" -eq 0 && "$staged_mysqlx_count" -eq 1 && "$live_mysqlx_count" -eq 1 ]] ||
        die 'mysqlx approved delta count mismatch'
    base_normalized=$(normalized_config_sha "$base80")
    staged_normalized=$(normalized_config_sha "$staged80")
    live_normalized=$(normalized_config_sha "$live80")
    printf 'NORMALIZED_MY80_TEMPLATE_SHA256 base=%s staged=%s live=%s\n' \
        "$base_normalized" "$staged_normalized" "$live_normalized"
    [[ "$base_normalized" == "$staged_normalized" && "$base_normalized" == "$live_normalized" ]] ||
        die 'my80.cnf has drift beyond mysqlx=OFF'
    printf 'MYSQLX_APPROVED_DELTA=PASS_ONLY_MYSQLX_OFF\n'
    awk -F '\t' -v node="$node" -v ip="$target_ip" -v host="$target_host" \
        -v profile="$expected_profile" -v clone="$expected_clone_source" \
        'NR > 1 && $1 == node && $2 == ip && $3 == host && $4 == profile && $5 == clone {found=1} END {exit !found}' \
        "$base/manifests/nodes.tsv" || die 'nodes.tsv row mismatch'
    printf 'Q160_MYSQL_TEMPLATE_SOURCE_AND_ASSET_GATE=PASS\n'
}

host_identity_evidence() {
    local machine_id ssh_key_count ssh_set_sha
    machine_id=$(tr -d '\n' </etc/machine-id)
    [[ "$machine_id" =~ ^[0-9a-f]{32}$ ]] || die 'machine-id format is invalid'
    ssh_key_count=$(find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*.pub' | wc -l | awk '{print $1}')
    [[ "$ssh_key_count" -ge 3 ]] || die 'SSH host public keys are incomplete'
    ssh_set_sha=$(sha256sum /etc/ssh/ssh_host_*.pub | sort | sha256sum | awk '{print $1}')
    printf 'MACHINE_ID_SHA256=%s\n' "$(printf '%s' "$machine_id" | sha256sum | awk '{print $1}')"
    printf 'SSH_HOST_PUBLIC_KEY_COUNT=%s\n' "$ssh_key_count"
    printf 'SSH_HOST_PUBLIC_KEY_SET_SHA256=%s\n' "$ssh_set_sha"
    printf 'Q160_MYSQL_HOST_IDENTITY_METADATA=PASS\n'
}

unit_and_listener_gate() {
    local unit enabled active port
    for unit in mysql56.service mysql80.service mysql-health56.service mysql-health80.service; 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 and active"
    done
    [[ $(pgrep -x mysqld | wc -l | awk '{print $1}') -eq 2 ]] || die 'exactly two mysqld processes are required'
    for port in 3306 3308 9201 9202; do
        ss -lntH | awk -v endpoint="$target_ip:$port" '$4 == endpoint {found=1} END {exit !found}' ||
            die "required listener is absent: $target_ip:$port"
        printf 'LISTENER=%s:%s STATUS=PASS\n' "$target_ip" "$port"
    done
    if ss -lntH | awk -v endpoint="$target_ip:33060" '$4 == endpoint {found=1} END {exit !found}'; then
        die 'MySQL X listener 33060 must remain absent'
    fi
    printf 'Q160_MYSQL_UNIT_PROCESS_LISTENER_GATE=PASS\n'
}

mysql_readonly_gate() {
    local client56=/opt/mysql/5.6.51/bin/mysql
    local client80=/opt/mysql/8.0.46/bin/mysql
    local identity56 identity80 repl56 repl80
    identity56=$("$client56" --no-defaults --protocol=socket --socket=/run/mysql56/mysql.sock \
        --user=root --batch --skip-column-names --connect-timeout=5 \
        -e "SELECT CONCAT(@@server_id,'|',@@server_uuid,'|',@@read_only)")
    identity80=$("$client80" --no-defaults --protocol=socket --socket=/run/mysql80/mysql.sock \
        --user=root --batch --skip-column-names --connect-timeout=5 \
        -e "SELECT CONCAT(@@server_id,'|',@@server_uuid,'|',@@read_only,'|',@@super_read_only)")
    IFS='|' read -r server56 uuid56 read56 <<<"$identity56"
    IFS='|' read -r server80 uuid80 read80 super80 <<<"$identity80"
    [[ "$server56" == "$expected56_id" && "$server80" == "$expected80_id" ]] || die 'server_id mismatch'
    [[ "$uuid56" =~ ^[0-9a-fA-F-]{36}$ && "$uuid80" =~ ^[0-9a-fA-F-]{36}$ ]] || die 'server_uuid format mismatch'
    [[ "$uuid56" != "$uuid80" ]] || die 'same-node MySQL UUID collision'
    [[ "$read56" == "$expected_read_only" && "$read80" == "$expected_read_only" ]] || die 'read_only role mismatch'
    [[ "$super80" == "$expected_read_only" ]] || die 'MySQL8 super_read_only role mismatch'
    printf 'MYSQL56_IDENTITY server_id=%s server_uuid=%s read_only=%s\n' "$server56" "$uuid56" "$read56"
    printf 'MYSQL80_IDENTITY server_id=%s server_uuid=%s read_only=%s super_read_only=%s\n' "$server80" "$uuid80" "$read80" "$super80"
    repl56=$("$client56" --no-defaults --protocol=socket --socket=/run/mysql56/mysql.sock \
        --user=root --batch --skip-column-names --connect-timeout=5 -e 'SHOW SLAVE STATUS')
    repl80=$("$client80" --no-defaults --protocol=socket --socket=/run/mysql80/mysql.sock \
        --user=root --batch --skip-column-names --connect-timeout=5 -e 'SHOW REPLICA STATUS')
    [[ -z "$repl56" ]] || die 'MySQL5.6 replication is configured before Q-221'
    [[ -z "$repl80" ]] || die 'MySQL8.0 replication is configured before Q-221'
    printf 'MYSQL56_REPLICATION_STATUS_ROWS=0\n'
    printf 'MYSQL80_REPLICATION_STATUS_ROWS=0\n'
    printf 'Q160_MYSQL_ROLE_AND_NO_REPLICATION_GATE=PASS\n'
}

forbidden_role_gate() {
    local unit active
    for unit in orchestrator.service keepalived.service sentinel5.service sentinel7.service; do
        active=$(systemctl is-active "$unit" 2>/dev/null || true)
        printf 'FORBIDDEN_UNIT=%s ACTIVE=%s\n' "$unit" "$active"
        [[ "$active" != active ]] || die "$unit must not be active on MySQL role"
    done
    printf 'Q160_MYSQL_FORBIDDEN_ROLE_GATE=PASS\n'
}

run_gate() {
    local command
    for command in awk bash cmp cut date find grep head hostname id ip pgrep sha256sum sort ss systemctl tr wc; do
        need "$command"
    done
    identity_gate
    receipt_gate
    role_asset_gate
    host_identity_evidence
    unit_and_listener_gate
    mysql_readonly_gate
    forbidden_role_gate
    printf 'Q160_MYSQL_NODE_READONLY_PASS\n'
    printf 'Q160_GLOBAL_PASS=NOT_ASSERTED_BY_MYSQL_ROLE\n'
}

case "$mode" in
    --static-validate) static_validate ;;
    --self-test-contract) self_test_contract ;;
    run) run_gate ;;
esac
