#!/usr/bin/env bash

set -Eeuo pipefail
umask 077
export LANG=C

EXPECTED_HOSTNAME="sqjcnginx01"
EXPECTED_IPV4="10.2.203.102"
EXPECTED_OS="openEuler release 24.03 (LTS-SP1)"
EXPECTED_SOURCE_COMMIT="47584918681b8aad36fee3c9eade5cd919c702ab"
EXPECTED_FRAMEWORK_SHA256="f74941f6328c752eeefb5fe082c4366dfed5c91ecad36d31ba7e11b7d2cac2d5"
EXPECTED_RPM_MANIFEST_SHA256="0ef1b62af801d782e8331b608b4b0580e0721a7553e913ab17686d4dc32dee3d"
BUNDLE_ROOT="/opt/zhct-deploy"
PACKAGE_ROOT="${BUNDLE_ROOT}/packages"
Q040_EVIDENCE="/var/backups/zhct-q040/20260722T203557+0800"
RUN_ID=${1:-}
EVIDENCE_DIR="/var/backups/zhct-q041/evidence/${RUN_ID}"
VIP_RE='10\.2\.203\.(115|116|117)'
MIN_FREE_BYTES=$((9 * 1024 * 1024 * 1024))

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

capture() {
  local name=$1
  shift
  {
    printf '# captured_at=%s\n' "$(date --iso-8601=seconds)"
    printf '# command='
    printf '%q ' "$@"
    printf '\n'
    "$@" || true
  } >"${EVIDENCE_DIR}/${name}.txt" 2>&1
}

[[ "${EUID}" -eq 0 ]] || die "must run as root"
[[ "${RUN_ID}" =~ ^q041-[0-9]{8}T[0-9]{6}[+]0800$ ]] || die "invalid Q041 run id"
[[ "$(hostname -s)" == "${EXPECTED_HOSTNAME}" ]] || die "hostname mismatch"
[[ "$(tr -d '\r' </etc/openEuler-release)" == "${EXPECTED_OS}" ]] || die "OS mismatch"
[[ "$(uname -m)" == "x86_64" ]] || die "CPU architecture mismatch"
ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1 | \
  grep -Fxq "${EXPECTED_IPV4}" || die "expected IPv4 is not configured"

install -d -m 0700 -o root -g root "${EVIDENCE_DIR}"
exec > >(tee -a "${EVIDENCE_DIR}/preinstall-check.log") 2>&1

for command in awk bash cmp curl cut dnf find flock grep install ip make rpm rsync sed sha256sum \
  ss systemctl tar tee; do
  command -v "${command}" >/dev/null 2>&1 || die "required preinstall command is missing: ${command}"
done

[[ "$(tr -d '\r\n' <"${BUNDLE_ROOT}/.source-commit")" == "${EXPECTED_SOURCE_COMMIT}" ]] || \
  die "framework source commit mismatch"
[[ "$(tr -d '\r\n' <"${BUNDLE_ROOT}/.framework-bundle-sha256")" == "${EXPECTED_FRAMEWORK_SHA256}" ]] || \
  die "framework bundle marker mismatch"
[[ "$(sha256sum "${PACKAGE_ROOT}/rpms.sha256" | awk '{print $1}')" == "${EXPECTED_RPM_MANIFEST_SHA256}" ]] || \
  die "RPM manifest SHA256 mismatch"
[[ $(wc -l <"${PACKAGE_ROOT}/rpms.sha256") -eq 18 ]] || die "RPM manifest must contain 18 rows"
[[ $(find "${PACKAGE_ROOT}/rpms" -maxdepth 1 -type f -name '*.rpm' | wc -l) -eq 18 ]] || \
  die "RPM directory must contain exactly 18 files"

(cd "${BUNDLE_ROOT}" && sha256sum -c roles/nginx/manifests/packages.sha256) \
  >"${EVIDENCE_DIR}/nginx-packages-sha256-check.txt"
(cd "${PACKAGE_ROOT}" && sha256sum -c rpms.sha256) \
  >"${EVIDENCE_DIR}/rpms-sha256-check.txt"
grep -Fq 'locked_asset_check=PASS' "${Q040_EVIDENCE}/v8-result.env" || \
  die "Q040 locked asset gate is not PASS"
grep -Fq 'openeuler_signature_check=PASS' "${Q040_EVIDENCE}/v8-result.env" || \
  die "Q040 openEuler signature gate is not PASS"
grep -Fq 'percona_signature_check=PASS' "${Q040_EVIDENCE}/v8-result.env" || \
  die "Q040 Percona signature gate is not PASS"
grep -Fq 'sensitive_scan=PASS' "${Q040_EVIDENCE}/v10-result.env" || \
  die "Q040 final sensitive scan is not PASS"

paths=(
  /usr/local/nginx
  /usr/local/haproxy
  /usr/local/sbin/keepalived
  /opt/redis/5.0.14
  /opt/redis/7.2.14
  /etc/zhct/templates/nginx
  /usr/local/sbin/check-front-vip.sh
  /usr/local/sbin/check-mqtt-vip.sh
  /usr/local/sbin/check-db-proxy-vip.sh
  /usr/local/sbin/vip-control.sh
  /usr/local/sbin/provision-edge-secrets.sh
  /etc/sysctl.d/99-zhct-edge.conf
  /etc/systemd/system/nginx.service
  /etc/systemd/system/keepalived.service
  /etc/systemd/system/haproxy.service
  /etc/systemd/system/sentinel5.service
  /etc/systemd/system/sentinel7.service
  /usr/lib/systemd/system/orchestrator.service
  /var/log/nginx
  /var/log/haproxy
  /var/log/redis-sentinel5
  /var/log/redis-sentinel7
  /var/lib/redis-sentinel5
  /var/lib/redis-sentinel7
  /var/lib/orchestrator
  /var/log/orchestrator
  /etc/orchestrator.conf.json
  /var/tmp/zhct-build/nginx
)
: >"${EVIDENCE_DIR}/path-state-before.tsv"
for path in "${paths[@]}"; do
  if [[ -e "${path}" || -L "${path}" ]]; then
    printf '%s\tPRESENT\n' "${path}" >>"${EVIDENCE_DIR}/path-state-before.tsv"
    die "target path already exists: ${path}"
  fi
  printf '%s\tABSENT\n' "${path}" >>"${EVIDENCE_DIR}/path-state-before.tsv"
done

: >"${EVIDENCE_DIR}/account-state-before.tsv"
for account in www haproxy redis orchestrator; do
  if id "${account}" >/dev/null 2>&1; then
    printf '%s\tPRESENT\n' "${account}" >>"${EVIDENCE_DIR}/account-state-before.tsv"
  else
    printf '%s\tABSENT\n' "${account}" >>"${EVIDENCE_DIR}/account-state-before.tsv"
  fi
done

target_rpms=$(rpm -q percona-orchestrator percona-orchestrator-cli \
  percona-orchestrator-client 2>/dev/null || true)
target_rpms=$(printf '%s\n' "${target_rpms}" | grep -Ev 'is not installed|未安装' || true)
[[ -z "${target_rpms}" ]] || die "target RPM is already installed"

target_ports=$(awk -F'\t' 'NR > 1 && $1 ~ /^[0-9]+$/ {
  ports[++count]=$1
} END {
  for (i=1; i<=count; i++) printf "%s%s", (i > 1 ? "|" : ""), ports[i]
}' "${BUNDLE_ROOT}/roles/nginx/manifests/ports.tsv")
target_listeners=$(ss -lntupH | grep -E ":(${target_ports})([[:space:]]|$)" || true)
target_processes=$(ps -eo pid,cmd | \
  grep -Ei '[n]ginx|[k]eepalived|[h]aproxy|[o]rchestrator|[r]edis-(server|sentinel)' || true)
vip_addresses=$(ip -o -4 addr show | grep -E "${VIP_RE}" || true)
[[ -z "${target_listeners}" ]] || die "target port is already listening"
[[ -z "${target_processes}" ]] || die "target process is already running"
[[ -z "${vip_addresses}" ]] || die "a project VIP is already announced"

capture dnf-history-before dnf history list
capture rpm-list-before rpm -qa
capture target-listeners-before ss -lntup
capture target-processes-before ps -eo pid,ppid,user,group,lstart,cmd
capture target-units-before systemctl list-unit-files --type=service --no-pager
capture ipv4-before ip -4 -br addr
capture disk-before df -hT
for rpm_file in "${PACKAGE_ROOT}"/percona-orchestrator*.rpm; do
  rpm -qpl "${rpm_file}"
done >"${EVIDENCE_DIR}/orchestrator-rpm-file-list.txt"

{
  printf 'net.ipv4.ip_nonlocal_bind\t%s\n' "$(sysctl -n net.ipv4.ip_nonlocal_bind)"
  printf 'net.ipv4.conf.all.arp_ignore\t%s\n' "$(sysctl -n net.ipv4.conf.all.arp_ignore)"
  printf 'net.ipv4.conf.all.arp_announce\t%s\n' "$(sysctl -n net.ipv4.conf.all.arp_announce)"
} >"${EVIDENCE_DIR}/sysctl-state-before.tsv"

mapfile -d '' rpms < <(find "${PACKAGE_ROOT}/rpms" -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z)
set +e
dnf --disablerepo='*' --setopt=install_weak_deps=False install --assumeno \
  "${rpms[@]}" >"${EVIDENCE_DIR}/dnf-install-assumeno.log" 2>&1
dnf_assumeno_rc=$?
set -e
[[ "${dnf_assumeno_rc}" -eq 1 ]] || die "offline DNF assumeno returned unexpected code ${dnf_assumeno_rc}"
grep -Eiq 'Transaction Summary|事务摘要' "${EVIDENCE_DIR}/dnf-install-assumeno.log" || \
  die "offline DNF assumeno has no transaction summary"
grep -Eq 'Install[[:space:]]+18 Package' "${EVIDENCE_DIR}/dnf-install-assumeno.log" || \
  die "offline DNF assumeno does not contain exactly Install 18"
if grep -Eiq '(^|[[:space:]])(Upgrading|Downgrading|Removing|Reinstalling|Obsoleting)(:|[[:space:]])|^[[:space:]]*(Upgrade|Downgrade|Remove|Reinstall|Obsolete)[[:space:]]+[0-9]+' \
  "${EVIDENCE_DIR}/dnf-install-assumeno.log"; then
  die "offline DNF transaction contains an action other than Install"
fi

"${BUNDLE_ROOT}/common/scripts/install-role.sh" \
  --role nginx \
  --bundle-root "${BUNDLE_ROOT}" \
  --dry-run >"${EVIDENCE_DIR}/role-install-dry-run.log" 2>&1
grep -Fq 'install completed for role=nginx dry_run=1' \
  "${EVIDENCE_DIR}/role-install-dry-run.log" || die "role dry-run did not complete"
if grep -Fq 'PENDING:' "${EVIDENCE_DIR}/role-install-dry-run.log"; then
  die "role dry-run contains a pending asset"
fi

root_available_bytes=$(df -B1 --output=avail / | tail -n 1 | tr -d ' ')
root_total_bytes=$(df -B1 --output=size / | tail -n 1 | tr -d ' ')
root_twenty_percent_bytes=$((root_total_bytes / 5))
minimum_required_bytes=${MIN_FREE_BYTES}
if ((root_twenty_percent_bytes > minimum_required_bytes)); then
  minimum_required_bytes=${root_twenty_percent_bytes}
fi
((root_available_bytes >= minimum_required_bytes)) || die "root free space is below the install gate"

dnf_history_id_before=$(dnf history list | awk '$1 ~ /^[0-9]+$/ {print $1; exit}')
dnf_history_id_before=${dnf_history_id_before:-0}
{
  printf 'captured_at=%s\n' "$(date --iso-8601=seconds)"
  printf 'run_id=%s\n' "${RUN_ID}"
  printf 'hostname_check=PASS\n'
  printf 'ip_os_arch_check=PASS\n'
  printf 'locked_asset_check=PASS\n'
  printf 'rpm_signature_evidence_check=PASS\n'
  printf 'rpm_count=18\n'
  printf 'dnf_transaction_actions=INSTALL_ONLY\n'
  printf 'role_install_dry_run=PASS\n'
  printf 'target_listener_count=0\n'
  printf 'target_process_count=0\n'
  printf 'target_rpm_count=0\n'
  printf 'vip_announcement_count=0\n'
  printf 'root_available_bytes=%s\n' "${root_available_bytes}"
  printf 'minimum_required_bytes=%s\n' "${minimum_required_bytes}"
  printf 'dnf_history_id_before=%s\n' "${dnf_history_id_before}"
  printf 'rollback_input_check=PASS\n'
  printf 'preinstall_check=PASS\n'
  printf 'install_execution=NOT_PERFORMED\n'
} >"${EVIDENCE_DIR}/preinstall-result.env"

chmod 0600 "${EVIDENCE_DIR}"/*
printf 'Q041_PREINSTALL_CHECK_OK\n'
cat "${EVIDENCE_DIR}/preinstall-result.env"
