#!/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=${Q192_AUDIT_ROOT:-/root/q192-mysql-readonly-audit}

usage() {
  printf '%s\n' \
    'Usage:' \
    '  q191-q192-mysql-resource-silent-readonly.sh --static-validate' \
    '  q191-q192-mysql-resource-silent-readonly.sh run --node mysql01|mysql02'
}

die() {
  printf 'Q192_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
    mysql01)
      target_host=sqjcDB01
      target_ip=10.2.208.106
      source_task=q121
      expected_server_id56=56106
      expected_server_id80=80106
      expected_read_only=OFF
      ;;
    mysql02)
      target_host=sqjcDB02
      target_ip=10.2.208.107
      source_task=q122
      expected_server_id56=56107
      expected_server_id80=80107
      expected_read_only=ON
      ;;
    *)
      die "unsupported node: $node"
      exit 2
      ;;
  esac
else
  usage
  exit 2
fi

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

if [[ "$mode" == run && ${Q192_LOG_WRAPPED:-0} != 1 ]]; then
  export Q192_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 'Q192_MODE_END=%s\n' "$mode"
  printf 'Q192_END_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
  printf 'Q192_TARGET_HOST=%s\n' "$target_host"
  printf 'Q192_TARGET_IP=%s\n' "$target_ip"
  printf 'Q192_TARGET_NODE=%s\n' "${node:-STATIC}"
  printf 'Q192_RC=%s\n' "$rc"
  printf 'Q192_ROLLBACK_STATE=NO_SYSTEM_CHANGE_TO_ROLL_BACK\n'
  if [[ "$rc" -eq 0 && "$mode" == --static-validate ]]; then
    printf 'Q192_JUDGMENT=LOCAL_STATIC_ONLY_REMOTE_NOT_EXECUTED\n'
  elif [[ "$rc" -eq 0 ]]; then
    printf 'Q192_JUDGMENT=MYSQL_NODE_RESOURCE_SILENT_READONLY_PASS_NOT_Q194_OR_Q200_PASS\n'
  else
    printf 'Q192_JUDGMENT=FAIL_CLOSED\n'
  fi
  return "$rc"
}
trap finish EXIT

printf 'Q192_MODE_BEGIN=%s\n' "$mode"
printf 'Q192_START_TIME=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
printf 'Q192_TARGET_HOST=%s\n' "$target_host"
printf 'Q192_TARGET_IP=%s\n' "$target_ip"
printf 'Q192_TARGET_NODE=%s\n' "${node:-STATIC}"
printf 'Q192_RUN_ID=%s\n' "$run_id"

static_validate() {
  local marker
  bash -n "$SCRIPT_PATH"
  for marker in \
    --static-validate \
    mysql01 \
    mysql02 \
    Q192_DEP_Q122 \
    Q192_DEP_Q191 \
    Q192_EXTERNAL_VM_RESOURCE_CONFIRMATION \
    Q192_CAPACITY_BUDGET_APPROVED \
    Q192_MIN_DATA_FREE_BYTES \
    Q192_MIN_BACKUP_FREE_BYTES \
    Q192_REMOTE_LOCK_OWNER \
    replication=NOT_PERFORMED_Q221_GATE \
    business_sql=NOT_PERFORMED_Q261_GATE
  do
    grep -Fq -- "$marker" "$SCRIPT_PATH" || die "missing marker: $marker"
  done
  printf 'Q192_MYSQL_LOCAL_STATIC_VALIDATE_PASS\n'
  printf 'Q192_REMOTE_STATUS=NOT_EXECUTED\n'
}

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

dependency_lock_gate() {
  [[ ${Q192_DEP_Q122:-} == PASS ]] || die 'Q-122 PASS is not asserted'
  [[ ${Q192_DEP_Q191:-} == PASS ]] || die 'Q-191 App resource PASS is not asserted'
  [[ ${Q192_EXTERNAL_VM_RESOURCE_CONFIRMATION:-} == PASS ]] ||
    die 'customer or virtualization resource confirmation is absent'
  [[ ${Q192_CAPACITY_BUDGET_APPROVED:-} == PASS ]] ||
    die 'MySQL capacity budget approval is absent'
  positive_integer "${Q192_MIN_DATA_FREE_BYTES:-}" ||
    die 'Q192_MIN_DATA_FREE_BYTES is missing or invalid'
  positive_integer "${Q192_MIN_BACKUP_FREE_BYTES:-}" ||
    die 'Q192_MIN_BACKUP_FREE_BYTES is missing or invalid'
  [[ ${Q192_REMOTE_LOCK_GRANTED:-} == 1 ]] || die 'remote lock is not asserted'
  [[ ${Q192_REMOTE_LOCK_OWNER:-} == MYSQL_Q192 ]] || die 'remote lock owner mismatch'
  printf 'Q192_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=mysql' /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 'Q192_IDENTITY_GATE=PASS\n'
}

q121_q122_receipt_gate() {
  local state="/var/lib/zhct-q121-122/${source_task}.last"
  local source_run result
  [[ -f "$state" && ! -L "$state" ]] || die "$source_task last-run state is absent"
  source_run=$(<"$state")
  [[ "$source_run" =~ ^${source_task}-[0-9]{8}T[0-9]{6}[+-][0-9]{4}$ ]] ||
    die "$source_task last-run state is invalid"
  result="/var/backups/zhct-q121-122/$source_run/result.env"
  [[ -f "$result" && ! -L "$result" ]] || die "$source_task PASS result is absent"
  grep -Fxq 'result=PASS' "$result" || die "$source_task result is not PASS"
  grep -Fxq 'replication=NOT_PERFORMED_Q221_GATE' "$result" ||
    die "$source_task Q-221 boundary is absent"
  grep -Fxq 'business_sql=NOT_PERFORMED_Q261_GATE' "$result" ||
    die "$source_task Q-261 boundary is absent"
  printf 'Q192_SOURCE_INITIALIZATION_RECEIPT=PASS TASK=%s RUN=%s\n' \
    "$source_task" "$source_run"
}

resource_size_gate() {
  local vcpu mem_bytes max_disk_bytes root_bytes
  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}')
  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"
  [[ "$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'
  printf 'Q192_MYSQL_RESOURCE_SIZE_GATE=PASS\n'
}

capacity_budget_gate() {
  local data_source backup_source data_free backup_free required_shared
  [[ -d /data && -d /var/backups ]] || die 'data or backup path is absent'
  data_source=$(findmnt -n -o SOURCE -T /data)
  backup_source=$(findmnt -n -o SOURCE -T /var/backups)
  data_free=$(df -B1 --output=avail /data | awk 'NR==2 {print $1}')
  backup_free=$(df -B1 --output=avail /var/backups | awk 'NR==2 {print $1}')
  printf 'CAPACITY_DATA_SOURCE=%s FREE_BYTES=%s REQUIRED=%s\n' \
    "$data_source" "$data_free" "$Q192_MIN_DATA_FREE_BYTES"
  printf 'CAPACITY_BACKUP_SOURCE=%s FREE_BYTES=%s REQUIRED=%s\n' \
    "$backup_source" "$backup_free" "$Q192_MIN_BACKUP_FREE_BYTES"
  if [[ "$data_source" == "$backup_source" ]]; then
    required_shared=$((Q192_MIN_DATA_FREE_BYTES + Q192_MIN_BACKUP_FREE_BYTES))
    printf 'CAPACITY_SHARED_FILESYSTEM=yes REQUIRED_COMBINED_BYTES=%s\n' "$required_shared"
    [[ "$data_free" -ge "$required_shared" ]] ||
      die 'shared data/backup filesystem free space is below the combined approved budget'
  else
    printf 'CAPACITY_SHARED_FILESYSTEM=no\n'
    [[ "$data_free" -ge "$Q192_MIN_DATA_FREE_BYTES" ]] ||
      die 'data filesystem free space is below approved budget'
    [[ "$backup_free" -ge "$Q192_MIN_BACKUP_FREE_BYTES" ]] ||
      die 'backup filesystem free space is below approved budget'
  fi
  printf 'Q192_MYSQL_CAPACITY_BUDGET_GATE=PASS\n'
}

resource_snapshot() {
  lscpu
  free -b
  lsblk -b -o NAME,TYPE,SIZE,FSTYPE,MOUNTPOINTS
  findmnt -R /
  df -B1 -T / /data /var/backups /var/log
  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 'Q192_RESOURCE_SNAPSHOT_CAPTURED=PASS\n'
}

config_value() {
  local file=$1 key=$2
  awk -F= -v wanted="$key" '
    /^[[:space:]]*[#;]/ {next}
    NF >= 2 {
      key=$1
      value=substr($0,index($0,"=")+1)
      gsub(/^[[:space:]]+|[[:space:]]+$/,"",key)
      gsub(/^[[:space:]]+|[[:space:]]+$/,"",value)
      if (key==wanted) print value
    }
  ' "$file"
}

single_config_value() {
  local file=$1 key=$2 expected=$3 value count
  count=$(config_value "$file" "$key" | awk 'END {print NR+0}')
  [[ "$count" -eq 1 ]] || die "config key count mismatch: $file $key"
  value=$(config_value "$file" "$key")
  [[ "$value" == "$expected" ]] ||
    die "config value mismatch: $file $key"
}

silent_config_unit_listener_gate() {
  local config56=/etc/my56.cnf config80=/etc/my80.cnf
  local unit enabled active listeners mysqld_count
  [[ $(stat -c '%a %U:%G' "$config56") == '640 root:mysql' ]] ||
    die 'my56.cnf metadata mismatch'
  [[ $(stat -c '%a %U:%G' "$config80") == '640 root:mysql' ]] ||
    die 'my80.cnf metadata mismatch'
  single_config_value "$config56" server-id "$expected_server_id56"
  single_config_value "$config80" server-id "$expected_server_id80"
  single_config_value "$config56" read-only "$expected_read_only"
  single_config_value "$config80" read-only "$expected_read_only"
  single_config_value "$config80" mysqlx OFF
  printf 'Q192_MYSQL_CONFIG_IDENTITY_READONLY_GATE=PASS\n'

  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 "required MySQL unit is not enabled/active: $unit"
  done
  listeners=$(ss -lntup)
  for port in 3306 3308 9201 9202; do
    grep -Eq "${target_ip}:${port}([[:space:]]|$)" <<<"$listeners" ||
      die "required listener is missing: $target_ip:$port"
  done
  ! grep -Eq ':33060([[:space:]]|$)' <<<"$listeners" ||
    die 'forbidden MySQL X listener is present'
  mysqld_count=$(pgrep -x mysqld | awk 'END {print NR+0}')
  [[ "$mysqld_count" -eq 2 ]] || die 'expected exactly two mysqld processes'
  printf 'Q192_MYSQL_SILENT_UNIT_LISTENER_GATE=PASS\n'
}

system_snapshot() {
  timedatectl show -p Timezone -p NTP -p NTPSynchronized
  ip -o -4 addr show
  ip route show table all
  if command -v firewall-cmd >/dev/null 2>&1; then
    firewall-cmd --state || true
    firewall-cmd --get-active-zones || true
  else
    printf 'FIREWALL_CMD=ABSENT_DEFER_Q200\n'
  fi
  printf 'Q192_SYSTEM_NETWORK_SNAPSHOT_READONLY=PASS\n'
}

run_gate() {
  dependency_lock_gate
  for command in awk bash chmod cut date df findmnt free grep hostname id ip lscpu \
    lsblk mkdir nproc pgrep sed ss stat systemctl tee timedatectl
  do
    need "$command"
  done
  identity_gate
  q121_q122_receipt_gate
  resource_size_gate
  capacity_budget_gate
  resource_snapshot
  silent_config_unit_listener_gate
  system_snapshot
  printf 'replication=NOT_PERFORMED_Q221_GATE\n'
  printf 'business_sql=NOT_PERFORMED_Q261_GATE\n'
  printf 'network_write=NOT_PERFORMED_Q200_GATE\n'
  printf 'Q192_MYSQL_NODE_RESOURCE_SILENT_READONLY_PASS\n'
  printf 'Q192_BOTH_MYSQL_NODES_PASS=NOT_ASSERTED_BY_NODE_SCRIPT\n'
  printf 'Q194_PASS=NOT_ASSERTED_BY_MYSQL_Q192_SCRIPT\n'
  printf 'Q200_PASS=NOT_ASSERTED_BY_MYSQL_Q192_SCRIPT\n'
}

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