#!/usr/bin/env bash

set -Eeuo pipefail
umask 077
export LANG=C

EXPECTED_HOSTNAME="sqjcapp01"
EXPECTED_IPV4="10.2.203.104"
EXPECTED_OS="openEuler release 24.03 (LTS-SP1)"
EXPECTED_SOURCE_COMMIT="fb48b4d4"
EXPECTED_FRAMEWORK_SHA256="d087c3b0fd1816a740da9d79682d4a5a2c3b6a5c55abd1776854d4e44dc6c5cb"
EXPECTED_PACKAGE_MANIFEST_SHA256="b65725b6da24e25ba2ab43678083332445f8d9296467d3d3f70f486cf001ed49"
EXPECTED_RPM_MANIFEST_SHA256="af7a889edac0ee48e8fad99d41fb6a8ee59ec619c5c571d510eed9c8f3d4e7a3"
EXPECTED_HOOK_SHA256="03c6b13a0def5bf3954e069581b6f9fbe0f81f24285ba2319b6279c0406d70aa"
EXPECTED_NGINX_UNIT_SHA256="f47b8a220c07a2873de7d5e1da89ad5e897167760bb5f5e4347553dcafe90fb7"
EXPECTED_REQUIREMENTS_SHA256="5dc1d831aefbe2b1359d3fe0aac55770b31f28cbb293dad668ca646b67b7d989"
EXPECTED_RPM_COUNT=43
MIN_FREE_BYTES=$((9 * 1024 * 1024 * 1024))

BUNDLE_ROOT="/opt/zhct-deploy"
PACKAGE_ROOT="${BUNDLE_ROOT}/packages"
RPM_ROOT="${PACKAGE_ROOT}/rpms"
ROLE_ROOT="${BUNDLE_ROOT}/roles/app"
RUN_ID=${1:-}
EVIDENCE_ROOT="/var/backups/zhct-q052/evidence"
EVIDENCE_DIR="${EVIDENCE_ROOT}/${RUN_ID}"
PIP_PROBE_ROOT="${EVIDENCE_DIR}/pip-probe"

TARGET_UNITS=(
  nginx.service
  php-fpm73.service
  php-fpm74.service
  supervisor.service
  crond.service
)

REQUIRED_PACKAGE_CAPABILITIES=(
  autoconf automake bison bzip2 c-ares-devel cmake cronie crontabs curl
  diffutils file findutils flex freetype-devel gawk gcc gcc-c++ gettext grep
  gzip libaio libcurl-devel libicu-devel libjpeg-turbo-devel libpng-devel
  libtool libwebp-devel libxml2-devel libzip-devel make ncurses-compat-libs
  oniguruma-devel openssl-devel patch pcre2-devel pkgconf-pkg-config python3
  python3-pip re2c rsync sed sqlite-devel systemd-devel tar which xz zlib-devel
)

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

cleanup() {
  local rc=$?
  trap - EXIT
  if [[ -n "${PIP_PROBE_ROOT:-}" && -e "${PIP_PROBE_ROOT}" ]]; then
    case "${PIP_PROBE_ROOT}" in
      /var/backups/zhct-q052/evidence/q052-*/pip-probe)
        rm -rf -- "${PIP_PROBE_ROOT}"
        ;;
      *)
        printf 'ERROR: refusing to clean unexpected pip probe path: %s\n' \
          "${PIP_PROBE_ROOT}" >&2
        exit 1
        ;;
    esac
  fi
  exit "${rc}"
}

assert_units_quiescent() {
  local output=$1
  : >"${output}"
  local unit enabled active substate
  for unit in "${TARGET_UNITS[@]}"; do
    enabled=$(systemctl is-enabled "${unit}" 2>/dev/null || true)
    active=$(systemctl show "${unit}" -p ActiveState --value)
    substate=$(systemctl show "${unit}" -p SubState --value)
    printf '%s\t%s\t%s\t%s\n' \
      "${unit}" "${enabled}" "${active}" "${substate}" >>"${output}"
    [[ "${enabled}" == "disabled" ]] || die "unit is not disabled: ${unit}"
    [[ "${active}" == "inactive" ]] || die "unit is not inactive: ${unit}"
    [[ "${substate}" == "dead" ]] || die "unit is not dead: ${unit}"
  done
}

assert_no_target_processes() {
  local output=$1
  : >"${output}"
  local process
  for process in nginx php-fpm supervisord mysqld mysqld_safe redis-server mosquitto; do
    if pgrep -a -x "${process}" >>"${output}" 2>&1; then
      die "target process is running: ${process}"
    fi
  done
}

validate_archives() {
  python3 - "${PACKAGE_ROOT}" "${EVIDENCE_DIR}/archive-safety.tsv" <<'PY'
import pathlib
import posixpath
import sys
import tarfile
import zipfile

root = pathlib.Path(sys.argv[1])
output = pathlib.Path(sys.argv[2])
archives = [
    "lnmp2.2-from-git-20260706.tar.gz",
    "nginx-1.26.3.tar.gz",
    "php-7.3.33.tar.bz2",
    "php-7.4.33.tar.bz2",
    "redis-5.3.5.tgz",
    "Mosquitto-0.4.0.tgz",
    "mosquitto-1.6.15.tar.gz",
    "redis-5.0.14.tar.gz",
    "mysql-5.6.51-linux-glibc2.12-x86_64.tar.gz",
]
wheel = "supervisor-4.3.0-py2.py3-none-any.whl"

def inside(parts):
    depth = 0
    for part in parts:
        if part in ("", "."):
            continue
        if part == "..":
            depth -= 1
            if depth < 0:
                return False
        else:
            depth += 1
    return True

rows = []
for name in archives:
    path = root / name
    with tarfile.open(path, "r:*") as archive:
        members = archive.getmembers()
        if not members:
            raise SystemExit(f"empty archive: {name}")
        for member in members:
            item = pathlib.PurePosixPath(member.name)
            if item.is_absolute() or not inside(item.parts):
                raise SystemExit(f"unsafe archive path: {name}:{member.name}")
            if member.isdev() or member.isfifo():
                raise SystemExit(f"forbidden special file: {name}:{member.name}")
            if member.issym():
                target = pathlib.PurePosixPath(member.linkname)
                combined = pathlib.PurePosixPath(
                    posixpath.normpath(str(item.parent / target))
                )
                if target.is_absolute() or not inside(combined.parts):
                    raise SystemExit(
                        f"unsafe symlink: {name}:{member.name}->{member.linkname}"
                    )
            if member.islnk():
                target = pathlib.PurePosixPath(member.linkname)
                if target.is_absolute() or not inside(target.parts):
                    raise SystemExit(
                        f"unsafe hardlink: {name}:{member.name}->{member.linkname}"
                    )
    rows.append((name, len(members), "PASS"))

with zipfile.ZipFile(root / wheel) as archive:
    members = archive.infolist()
    if not members:
        raise SystemExit(f"empty wheel: {wheel}")
    for member in members:
        item = pathlib.PurePosixPath(member.filename)
        if item.is_absolute() or not inside(item.parts):
            raise SystemExit(f"unsafe wheel path: {wheel}:{member.filename}")
rows.append((wheel, len(members), "PASS"))

output.write_text(
    "artifact\tmember_count\tpath_and_link_safety\n"
    + "".join(f"{name}\t{count}\t{status}\n" for name, count, status in rows)
)
PY
}

[[ "${EUID}" -eq 0 ]] || die "must run as root"
[[ "${RUN_ID}" =~ ^q052-[0-9]{8}T[0-9]{6}[+]0800(-R[1-9][0-9]*)?$ ]] || \
  die "invalid Q052 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"
global_ipv4s=$(ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1)
grep -Fxq "${EXPECTED_IPV4}" <<<"${global_ipv4s}" || \
  die "expected IPv4 is not configured"

for command in awk bash cat chmod cmp curl cut date df dnf file find grep \
  hostname install ip ldd mkdir pgrep python3 readelf realpath rm rpm sed \
  sha256sum sort ss stat systemctl tail tar tee timeout tr uname unshare wc; do
  command -v "${command}" >/dev/null 2>&1 || die "required command is missing: ${command}"
done

install -d -m 0700 -o root -g root "${EVIDENCE_ROOT}"
mkdir -m 0700 "${EVIDENCE_DIR}" 2>/dev/null || \
  die "Q052 run id has already been claimed: ${RUN_ID}"
trap cleanup EXIT
exec > >(tee "${EVIDENCE_DIR}/preflight-check.log") 2>&1

SELF_SHA256=$(sha256sum "$0" | awk '{print $1}')
printf 'preflight_script_sha256=%s\n' "${SELF_SHA256}"

[[ "$(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 marker mismatch"
[[ "$(sha256sum "${ROLE_ROOT}/manifests/packages.sha256" | awk '{print $1}')" == \
  "${EXPECTED_PACKAGE_MANIFEST_SHA256}" ]] || die "package manifest SHA256 mismatch"
[[ "$(sha256sum "${PACKAGE_ROOT}/rpms.sha256" | awk '{print $1}')" == \
  "${EXPECTED_RPM_MANIFEST_SHA256}" ]] || die "RPM manifest SHA256 mismatch"
[[ "$(sha256sum "${ROLE_ROOT}/hooks.sh" | awk '{print $1}')" == \
  "${EXPECTED_HOOK_SHA256}" ]] || die "App hook SHA256 mismatch"
[[ "$(sha256sum "${ROLE_ROOT}/templates/systemd/nginx.service" | awk '{print $1}')" == \
  "${EXPECTED_NGINX_UNIT_SHA256}" ]] || die "Nginx template SHA256 mismatch"
[[ "$(sha256sum /etc/systemd/system/nginx.service | awk '{print $1}')" == \
  "${EXPECTED_NGINX_UNIT_SHA256}" ]] || die "Nginx system unit SHA256 mismatch"
[[ "$(sha256sum "${ROLE_ROOT}/templates/python/requirements-supervisor.txt" | \
  awk '{print $1}')" == "${EXPECTED_REQUIREMENTS_SHA256}" ]] || \
  die "Supervisor requirements SHA256 mismatch"
grep -Fxq '/usr/local/lib' /etc/ld.so.conf.d/zhct-local.conf || \
  die "local loader configuration is missing"
ldconfig -p >"${EVIDENCE_DIR}/ldconfig-cache.txt" 2>&1 || \
  die "failed to capture dynamic loader cache"
grep -Fq 'libmosquitto.so.1' "${EVIDENCE_DIR}/ldconfig-cache.txt" || \
  die "libmosquitto loader cache entry is missing"

(cd "${BUNDLE_ROOT}" && sha256sum -c roles/app/manifests/packages.sha256) \
  >"${EVIDENCE_DIR}/app-packages-sha256-check.txt"
(cd "${PACKAGE_ROOT}" && sha256sum -c rpms.sha256) \
  >"${EVIDENCE_DIR}/rpms-sha256-check.txt"

rpm_count=$(find "${RPM_ROOT}" -maxdepth 1 -type f -name '*.rpm' | wc -l)
rpm_manifest_count=$(wc -l <"${PACKAGE_ROOT}/rpms.sha256")
[[ "${rpm_count}" -eq "${EXPECTED_RPM_COUNT}" ]] || die "RPM file count mismatch"
[[ "${rpm_manifest_count}" -eq "${EXPECTED_RPM_COUNT}" ]] || \
  die "RPM manifest row count mismatch"
find "${RPM_ROOT}" -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z | \
  xargs -0 rpm --define '_pkgverify_level signature' -K \
  >"${EVIDENCE_DIR}/rpm-signatures.txt" 2>&1
[[ "$(grep -Ec 'signatures OK$' "${EVIDENCE_DIR}/rpm-signatures.txt")" -eq \
  "${EXPECTED_RPM_COUNT}" ]] || die "not every RPM has a trusted signature"
if grep -Eiq 'BAD|NOT OK|NOKEY|NOTTRUSTED' "${EVIDENCE_DIR}/rpm-signatures.txt"; then
  die "an RPM signature is not trusted"
fi

validate_archives

: >"${EVIDENCE_DIR}/required-package-providers.tsv"
for capability in "${REQUIRED_PACKAGE_CAPABILITIES[@]}"; do
  provider=$(rpm -q --whatprovides --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}.%{ARCH}\n' \
    "${capability}" | sort -u) || die "required package capability is missing: ${capability}"
  printf '%s\t%s\n' "${capability}" "${provider//$'\n'/;}" \
    >>"${EVIDENCE_DIR}/required-package-providers.tsv"
done

mapfile -d '' rpms < <(
  find "${RPM_ROOT}" -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z
)
: >"${EVIDENCE_DIR}/locked-rpm-nevra.tsv"
: >"${EVIDENCE_DIR}/installed-rpm-nevra.tsv"
for rpm_file in "${rpms[@]}"; do
  locked=$(rpm -qp --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}.%{ARCH}' \
    "${rpm_file}")
  package_name=${locked%%$'\t'*}
  printf '%s\n' "${locked}" >>"${EVIDENCE_DIR}/locked-rpm-nevra.tsv"
  rpm -q --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}.%{ARCH}\n' \
    "${package_name}" | grep -Fx "${locked}" \
    >>"${EVIDENCE_DIR}/installed-rpm-nevra.tsv" || \
    die "locked RPM NEVRA is not installed: ${locked}"
done
sort -u -o "${EVIDENCE_DIR}/locked-rpm-nevra.tsv" \
  "${EVIDENCE_DIR}/locked-rpm-nevra.tsv"
sort -u -o "${EVIDENCE_DIR}/installed-rpm-nevra.tsv" \
  "${EVIDENCE_DIR}/installed-rpm-nevra.tsv"
cmp "${EVIDENCE_DIR}/locked-rpm-nevra.tsv" \
  "${EVIDENCE_DIR}/installed-rpm-nevra.tsv" || \
  die "installed RPM identity differs from locked closure"
[[ "$(wc -l <"${EVIDENCE_DIR}/locked-rpm-nevra.tsv")" -eq \
  "${EXPECTED_RPM_COUNT}" ]] || die "locked RPM names are not unique"

php73_extension_dir=$(/usr/local/php7.3/bin/php-config --extension-dir)
php74_extension_dir=$(/usr/local/php7.4/bin/php-config --extension-dir)
FORMAL_FILES=(
  /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
  "${php73_extension_dir}/redis.so"
  "${php73_extension_dir}/mosquitto.so"
  "${php74_extension_dir}/redis.so"
  "${php74_extension_dir}/mosquitto.so"
  /usr/local/lib/libmosquitto.so.1
  /usr/local/bin/mosquitto_pub
  /usr/local/bin/mosquitto_sub
  /opt/redis-client/5.0.14/bin/redis-cli
  /opt/mysql-client/5.6.51/bin/mysql
  /opt/mysql-client/5.6.51/bin/mysqldump
  /opt/mysql-client/5.6.51/bin/mysqladmin
  /usr/local/bin/supervisord
  "${ROLE_ROOT}/hooks.sh"
  "${ROLE_ROOT}/templates/systemd/nginx.service"
  /etc/systemd/system/nginx.service
  /etc/systemd/system/php-fpm73.service
  /etc/systemd/system/php-fpm74.service
  /etc/systemd/system/supervisor.service
  /etc/ld.so.conf.d/zhct-local.conf
)
for path in "${FORMAL_FILES[@]}"; do
  [[ -f "${path}" && ! -L "${path}" ]] || die "formal file missing or unsafe: ${path}"
done

assert_units_quiescent "${EVIDENCE_DIR}/unit-state-before.tsv"
assert_no_target_processes "${EVIDENCE_DIR}/target-processes-before.txt"
ss -lntupH >"${EVIDENCE_DIR}/listeners-before.txt"
ip -4 -br addr >"${EVIDENCE_DIR}/ipv4-before.txt"
ip route show table all >"${EVIDENCE_DIR}/routes-before.txt"
sha256sum "${FORMAL_FILES[@]}" >"${EVIDENCE_DIR}/formal-files-before.sha256"
[[ -z "$(ss -lntupH | grep -E ':(8081|18081)([[:space:]]|$)' || true)" ]] || \
  die "App target port is listening before Q052"
[[ ! -e /workspace/wwwroot/zhct_sanquan &&
  ! -L /workspace/wwwroot/zhct_sanquan ]] || die "formal application root exists"

: >"${EVIDENCE_DIR}/formal-ldd.txt"
for binary 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 \
  "${php73_extension_dir}/redis.so" \
  "${php73_extension_dir}/mosquitto.so" \
  "${php74_extension_dir}/redis.so" \
  "${php74_extension_dir}/mosquitto.so" \
  /usr/local/bin/mosquitto_pub \
  /usr/local/bin/mosquitto_sub \
  /opt/redis-client/5.0.14/bin/redis-cli \
  /opt/mysql-client/5.6.51/bin/mysql \
  /opt/mysql-client/5.6.51/bin/mysqldump \
  /opt/mysql-client/5.6.51/bin/mysqladmin; do
  printf 'FILE=%s\n' "${binary}" >>"${EVIDENCE_DIR}/formal-ldd.txt"
  ldd "${binary}" >>"${EVIDENCE_DIR}/formal-ldd.txt" 2>&1
done
! grep -Fq 'not found' "${EVIDENCE_DIR}/formal-ldd.txt" || \
  die "formal binary has an unresolved dynamic library"

unshare -n true >"${EVIDENCE_DIR}/network-namespace-capability.txt" 2>&1 || \
  die "network namespace isolation is unavailable"
set +e
timeout 6 unshare -n bash -c \
  'ip link set lo up; curl --noproxy "*" --connect-timeout 2 --max-time 4 -fsS https://repo.openeuler.org/ >/dev/null' \
  >"${EVIDENCE_DIR}/isolated-public-network-negative-test.txt" 2>&1
isolated_curl_rc=$?
set -e
[[ "${isolated_curl_rc}" -ne 0 ]] || \
  die "public network unexpectedly worked inside isolated namespace"

dnf_history_id_before=$(dnf history list | \
  awk '$1 ~ /^[0-9]+$/ && !found {print $1; found=1}')
dnf_history_id_before=${dnf_history_id_before:-0}
set +e
unshare -n dnf --disablerepo='*' --cacheonly \
  --setopt=install_weak_deps=False \
  --setopt=localpkg_gpgcheck=True \
  reinstall --assumeno "${rpms[@]}" \
  >"${EVIDENCE_DIR}/dnf-cacheonly-reinstall-assumeno.log" 2>&1
dnf_assumeno_rc=$?
set -e
[[ "${dnf_assumeno_rc}" -eq 1 ]] || \
  die "cache-only DNF assumeno returned unexpected code ${dnf_assumeno_rc}"
grep -Fq 'Transaction Summary' \
  "${EVIDENCE_DIR}/dnf-cacheonly-reinstall-assumeno.log" || \
  die "cache-only DNF output has no transaction summary"
grep -Fq 'Reinstalling:' \
  "${EVIDENCE_DIR}/dnf-cacheonly-reinstall-assumeno.log" || \
  die "cache-only DNF output has no Reinstalling section"
grep -Fxq 'Operation aborted.' \
  "${EVIDENCE_DIR}/dnf-cacheonly-reinstall-assumeno.log" || \
  die "cache-only DNF did not terminate with Operation aborted"
if grep -Eiq 'Failed to download|Cannot download|Curl error|Could not resolve host|No match for argument|conflicting requests|nothing provides|Transaction test error' \
  "${EVIDENCE_DIR}/dnf-cacheonly-reinstall-assumeno.log"; then
  die "cache-only DNF has an unresolved package or network dependency"
fi
if grep -Eq '^[[:space:]]*(Installing|Upgrading|Removing|Downgrading|Skipping|Obsoleting|Erasing|Replacing)([^:]*)?:[[:space:]]*$' \
  "${EVIDENCE_DIR}/dnf-cacheonly-reinstall-assumeno.log"; then
  die "cache-only DNF contains a non-reinstall transaction action"
fi
cut -f1 "${EVIDENCE_DIR}/locked-rpm-nevra.tsv" | sort -u \
  >"${EVIDENCE_DIR}/dnf-expected-reinstall-packages.txt"
awk '
  /^[[:space:]]*Reinstalling:[[:space:]]*$/ {
    in_reinstall = 1
    next
  }
  /^[[:space:]]*Transaction Summary[[:space:]]*$/ {
    in_reinstall = 0
  }
  in_reinstall {
    line = $0
    sub(/^[[:space:]]+/, "", line)
    if (line == "" || line ~ /^=+$/) {
      next
    }
    split(line, fields, /[[:space:]]+/)
    print fields[1]
  }
' "${EVIDENCE_DIR}/dnf-cacheonly-reinstall-assumeno.log" \
  >"${EVIDENCE_DIR}/dnf-actual-reinstall-packages.raw.txt"
sort -u "${EVIDENCE_DIR}/dnf-actual-reinstall-packages.raw.txt" \
  >"${EVIDENCE_DIR}/dnf-actual-reinstall-packages.txt"
[[ "$(wc -l <"${EVIDENCE_DIR}/dnf-expected-reinstall-packages.txt")" -eq \
  "${EXPECTED_RPM_COUNT}" ]] || \
  die "expected DNF reinstall package set does not contain 43 unique names"
[[ "$(wc -l <"${EVIDENCE_DIR}/dnf-actual-reinstall-packages.raw.txt")" -eq \
  "${EXPECTED_RPM_COUNT}" ]] || \
  die "actual DNF Reinstalling section does not contain exactly 43 package rows"
[[ "$(wc -l <"${EVIDENCE_DIR}/dnf-actual-reinstall-packages.txt")" -eq \
  "${EXPECTED_RPM_COUNT}" ]] || \
  die "actual DNF reinstall package set does not contain 43 unique names"
cmp "${EVIDENCE_DIR}/dnf-expected-reinstall-packages.txt" \
  "${EVIDENCE_DIR}/dnf-actual-reinstall-packages.txt" || \
  die "actual DNF reinstall package set differs from the locked 43 RPMs"

install -d -m 0700 -o root -g root \
  "${PIP_PROBE_ROOT}" "${PIP_PROBE_ROOT}/home" "${PIP_PROBE_ROOT}/tmp" \
  "${PIP_PROBE_ROOT}/target" "${PIP_PROBE_ROOT}/assets"
install -m 0600 -o root -g root \
  "${PACKAGE_ROOT}/supervisor-4.3.0-py2.py3-none-any.whl" \
  "${PIP_PROBE_ROOT}/assets/supervisor-4.3.0-py2.py3-none-any.whl"
install -m 0600 -o root -g root \
  "${ROLE_ROOT}/templates/python/requirements-supervisor.txt" \
  "${PIP_PROBE_ROOT}/assets/requirements-supervisor.txt"
unshare -n env -i \
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
    HOME="${PIP_PROBE_ROOT}/home" \
    TMPDIR="${PIP_PROBE_ROOT}/tmp" \
    PIP_CONFIG_FILE=/dev/null \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PYTHONNOUSERSITE=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    LANG=C \
  python3 -m pip install \
    --no-index --no-deps --require-hashes \
    --find-links "${PIP_PROBE_ROOT}/assets" \
    --target "${PIP_PROBE_ROOT}/target" \
    -r "${PIP_PROBE_ROOT}/assets/requirements-supervisor.txt" \
    >"${EVIDENCE_DIR}/pip-offline-probe.log" 2>&1
PYTHONPATH="${PIP_PROBE_ROOT}/target" PYTHONDONTWRITEBYTECODE=1 \
  python3 -c 'import supervisor; print(supervisor.__file__)' \
  >"${EVIDENCE_DIR}/pip-offline-import-path.txt"
grep -Fq "${PIP_PROBE_ROOT}/target/supervisor/__init__.py" \
  "${EVIDENCE_DIR}/pip-offline-import-path.txt" || \
  die "offline Supervisor module was not imported from the temporary target"
mapfile -t supervisor_metadata_files < <(
  find "${PIP_PROBE_ROOT}/target" -maxdepth 2 -type f \
    -path '*/supervisor-*.dist-info/METADATA' -print
)
[[ "${#supervisor_metadata_files[@]}" -eq 1 ]] || \
  die "temporary target does not contain exactly one Supervisor METADATA file"
grep -Fxq 'Name: supervisor' "${supervisor_metadata_files[0]}" || \
  die "temporary Supervisor distribution name check failed"
grep -Fxq 'Version: 4.3.0' "${supervisor_metadata_files[0]}" || \
  die "temporary Supervisor distribution version check failed"
{
  printf 'metadata_path=%s\n' "${supervisor_metadata_files[0]}"
  grep -E '^(Name|Version): ' "${supervisor_metadata_files[0]}"
} >"${EVIDENCE_DIR}/pip-offline-distribution-metadata.txt"
rm -rf -- "${PIP_PROBE_ROOT}"

env RUN_ID="${RUN_ID}-ROLE-VERIFY" BACKUP_ROOT="/var/backups/zhct-q052" \
  bash -c 'ulimit -n 65536; exec /opt/zhct-deploy/common/scripts/verify-role.sh --role app --bundle-root /opt/zhct-deploy' \
  >"${EVIDENCE_DIR}/role-verify.log" 2>&1

dnf_history_id_after=$(dnf history list | \
  awk '$1 ~ /^[0-9]+$/ && !found {print $1; found=1}')
dnf_history_id_after=${dnf_history_id_after:-0}
[[ "${dnf_history_id_after}" == "${dnf_history_id_before}" ]] || \
  die "DNF history changed during Q052 preflight"

assert_units_quiescent "${EVIDENCE_DIR}/unit-state-after.tsv"
assert_no_target_processes "${EVIDENCE_DIR}/target-processes-after.txt"
ss -lntupH >"${EVIDENCE_DIR}/listeners-after.txt"
ip -4 -br addr >"${EVIDENCE_DIR}/ipv4-after.txt"
ip route show table all >"${EVIDENCE_DIR}/routes-after.txt"
sha256sum "${FORMAL_FILES[@]}" >"${EVIDENCE_DIR}/formal-files-after.sha256"

cmp "${EVIDENCE_DIR}/unit-state-before.tsv" \
  "${EVIDENCE_DIR}/unit-state-after.tsv" || die "unit state changed during preflight"
cmp "${EVIDENCE_DIR}/target-processes-before.txt" \
  "${EVIDENCE_DIR}/target-processes-after.txt" || \
  die "target process state changed during preflight"
cmp "${EVIDENCE_DIR}/listeners-before.txt" \
  "${EVIDENCE_DIR}/listeners-after.txt" || die "listeners changed during preflight"
cmp "${EVIDENCE_DIR}/ipv4-before.txt" \
  "${EVIDENCE_DIR}/ipv4-after.txt" || die "IPv4 state changed during preflight"
cmp "${EVIDENCE_DIR}/routes-before.txt" \
  "${EVIDENCE_DIR}/routes-after.txt" || die "routes changed during preflight"
cmp "${EVIDENCE_DIR}/formal-files-before.sha256" \
  "${EVIDENCE_DIR}/formal-files-after.sha256" || \
  die "formal file changed during preflight"
[[ -z "$(ss -lntupH | grep -E ':(8081|18081)([[:space:]]|$)' || true)" ]] || \
  die "App target port is listening after Q052 preflight"
[[ ! -e /workspace/wwwroot/zhct_sanquan &&
  ! -L /workspace/wwwroot/zhct_sanquan ]] || \
  die "formal application root appeared during Q052 preflight"

root_available_bytes=$(df -B1 --output=avail / | tail -n 1 | tr -d ' ')
root_total_bytes=$(df -B1 --output=size / | tail -n 1 | tr -d ' ')
minimum_required_bytes=${MIN_FREE_BYTES}
root_twenty_percent_bytes=$((root_total_bytes / 5))
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 Q052 gate"

{
  printf 'captured_at=%s\n' "$(date --iso-8601=seconds)"
  printf 'run_id=%s\n' "${RUN_ID}"
  printf 'preflight_script_sha256=%s\n' "${SELF_SHA256}"
  printf 'host_identity_check=PASS\n'
  printf 'q051_live_closeout_state=PASS\n'
  printf 'locked_asset_count=10\n'
  printf 'locked_asset_sha256_check=PASS\n'
  printf 'archive_path_and_link_safety=PASS\n'
  printf 'rpm_count=%s\n' "${EXPECTED_RPM_COUNT}"
  printf 'rpm_sha256_check=PASS\n'
  printf 'rpm_signature_check=PASS\n'
  printf 'required_package_capability_check=PASS\n'
  printf 'locked_rpm_installed_nevra_identity=PASS\n'
  printf 'network_namespace_capability=PASS\n'
  printf 'isolated_public_network=BLOCKED\n'
  printf 'isolated_public_network_test_rc=%s\n' "${isolated_curl_rc}"
  printf 'dnf_cacheonly_assumeno_rc=%s\n' "${dnf_assumeno_rc}"
  printf 'dnf_cacheonly_reinstall_package_count=%s\n' "${EXPECTED_RPM_COUNT}"
  printf 'dnf_cacheonly_reinstall_resolution=PASS\n'
  printf 'pip_no_index_no_deps_require_hashes_probe=PASS\n'
  printf 'supervisor_offline_wheel_version=4.3.0\n'
  printf 'formal_dynamic_link_check=PASS\n'
  printf 'app_role_verify=PASS\n'
  printf 'dnf_history_id_before=%s\n' "${dnf_history_id_before}"
  printf 'dnf_history_id_after=%s\n' "${dnf_history_id_after}"
  printf 'formal_file_immutability=PASS\n'
  printf 'unit_state_immutability=PASS\n'
  printf 'host_listener_immutability=PASS\n'
  printf 'host_ipv4_immutability=PASS\n'
  printf 'host_route_immutability=PASS\n'
  printf 'target_units_quiescent=PASS\n'
  printf 'target_listener_count=0\n'
  printf 'target_process_count=0\n'
  printf 'formal_application_root=ABSENT\n'
  printf 'root_available_bytes=%s\n' "${root_available_bytes}"
  printf 'minimum_required_bytes=%s\n' "${minimum_required_bytes}"
  printf 'preflight_check=PASS\n'
  printf 'offline_rebuild_execution=NOT_PERFORMED\n'
} >"${EVIDENCE_DIR}/preflight-result.env"

find "${EVIDENCE_DIR}" -type d -exec chmod 0700 {} +
find "${EVIDENCE_DIR}" -type f -exec chmod 0600 {} +
trap - EXIT
printf 'Q052_PREFLIGHT_CHECK_OK\n'
cat "${EVIDENCE_DIR}/preflight-result.env"
