#!/usr/bin/env bash

set -Eeuo pipefail
export LANG=C

EXPECTED_HOSTNAME="sqjcapp01"
EXPECTED_IPV4="10.2.203.104"
EXPECTED_OS="openEuler release 24.03 (LTS-SP1)"
EXPECTED_ARCH="x86_64"
EXPECTED_FRAMEWORK_SHA256="d087c3b0fd1816a740da9d79682d4a5a2c3b6a5c55abd1776854d4e44dc6c5cb"
BACKUP_ROOT="/var/backups/zhct-q050"
BUNDLE_ROOT="/opt/zhct-deploy"
PACKAGE_ROOT="${BUNDLE_ROOT}/packages"
MIN_FREE_BYTES=$((25 * 1024 * 1024 * 1024))
TARGET_PORTS_RE=':(8081|18081)([[:space:]]|$)'
TARGET_UNITS=(nginx php-fpm73 php-fpm74 supervisor)

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

value_from() {
  local file="$1"
  local key="$2"
  awk -F= -v wanted="${key}" '$1 == wanted {sub(/^[^=]*=/, ""); print; exit}' "${file}"
}

assert_archive_members_safe() {
  local archive="$1"
  python3 - "${archive}" <<'PY'
import pathlib
import sys
import tarfile

archive = pathlib.Path(sys.argv[1])
with tarfile.open(archive, "r:*") as tf:
    members = tf.getmembers()
    if not members:
        raise SystemExit(f"empty archive: {archive}")
    for member in members:
        path = pathlib.PurePosixPath(member.name)
        if path.is_absolute() or ".." in path.parts:
            raise SystemExit(f"unsafe archive member: {member.name}")
PY
}

[[ "${EUID}" -eq 0 ]] || die "must run as root"
[[ $# -eq 1 ]] || die "usage: $0 <q050-baseline-run-id>"
RUN_ID="$1"
[[ "${RUN_ID}" =~ ^[0-9]{8}T[0-9]{6}[+-][0-9]{4}$ ]] || die "invalid run id"
EVIDENCE_DIR="${BACKUP_ROOT}/${RUN_ID}"
SAFE_DIR="${EVIDENCE_DIR}/offline-assets-safe-evidence"
ARCHIVE="/root/q050-evidence-${RUN_ID}.tar.gz"

[[ "$(hostname -s)" == "${EXPECTED_HOSTNAME}" ]] || die "hostname mismatch"
[[ "$(uname -m)" == "${EXPECTED_ARCH}" ]] || die "architecture mismatch"
[[ "$(tr -d '\r' </etc/openEuler-release)" == "${EXPECTED_OS}" ]] || die "OS mismatch"
ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1 | \
  grep -Fxq "${EXPECTED_IPV4}" || die "expected IPv4 is not configured"

for required in \
  "${EVIDENCE_DIR}/result.env" \
  "${EVIDENCE_DIR}/sha256sums.txt" \
  "${EVIDENCE_DIR}/framework-fb48b4d4-refresh-result.env" \
  "${EVIDENCE_DIR}/source-assets-result.env" \
  "${EVIDENCE_DIR}/source-assets-inventory.tsv" \
  "${EVIDENCE_DIR}/source-assets.sha256" \
  "${EVIDENCE_DIR}/mysql-archive-members.txt" \
  "${EVIDENCE_DIR}/rpm-closure-result.env" \
  "${EVIDENCE_DIR}/rpm-nevra.tsv" \
  "${EVIDENCE_DIR}/rpm-signatures.txt" \
  "${EVIDENCE_DIR}/dnf-offline-closure-assumeno.log" \
  "${PACKAGE_ROOT}/rpms.sha256"; do
  [[ -s "${required}" ]] || die "required Q-050 evidence is missing: ${required}"
done

grep -Fxq 'target_listener_check=PASS' "${EVIDENCE_DIR}/result.env" || \
  die "baseline target listener gate is not PASS"
grep -Fxq 'source_asset_download=PASS' "${EVIDENCE_DIR}/source-assets-result.env" || \
  die "source asset result is not PASS"
grep -Fxq 'rpm_sha256_check=PASS' "${EVIDENCE_DIR}/rpm-closure-result.env" || \
  die "RPM SHA256 result is not PASS"
grep -Fxq 'rpm_signature_check=PASS' "${EVIDENCE_DIR}/rpm-closure-result.env" || \
  die "RPM signature result is not PASS"
grep -Fxq 'dnf_transaction_actions=INSTALL_ONLY' "${EVIDENCE_DIR}/rpm-closure-result.env" || \
  die "RPM closure is not install-only"
grep -Fxq 'dnf_offline_dependency_resolution=PASS_ASSUMENO' \
  "${EVIDENCE_DIR}/rpm-closure-result.env" || die "offline DNF result is not PASS"
grep -Fxq 'install_execution=NOT_PERFORMED' "${EVIDENCE_DIR}/rpm-closure-result.env" || \
  die "RPM closure records an installation"

[[ "$(tr -d '\r\n' <"${BUNDLE_ROOT}/.framework-bundle-sha256")" == \
  "${EXPECTED_FRAMEWORK_SHA256}" ]] || die "framework marker mismatch"
[[ "$(tr -d '\r\n' <"${BUNDLE_ROOT}/.source-commit")" == "fb48b4d4" ]] || \
  die "framework source marker mismatch"

(
  cd "${BUNDLE_ROOT}"
  sha256sum -c roles/app/manifests/packages.sha256
) >"${EVIDENCE_DIR}/locked-source-sha256-check.txt"

MYSQL_SHA256="$(value_from "${EVIDENCE_DIR}/source-assets-result.env" mysql_archive_sha256)"
[[ "${MYSQL_SHA256}" =~ ^[0-9a-f]{64}$ ]] || die "invalid MySQL SHA256 in source result"
[[ "$(sha256sum "${PACKAGE_ROOT}/mysql-5.6.51-linux-glibc2.12-x86_64.tar.gz" | \
  awk '{print $1}')" == "${MYSQL_SHA256}" ]] || die "MySQL archive SHA256 mismatch"
for member in mysql mysqldump mysqladmin; do
  grep -Fxq "mysql-5.6.51-linux-glibc2.12-x86_64/bin/${member}" \
    "${EVIDENCE_DIR}/mysql-archive-members.txt" || die "MySQL member is missing: ${member}"
done

RPM_MANIFEST_SHA256="$(value_from "${EVIDENCE_DIR}/rpm-closure-result.env" rpm_manifest_sha256)"
[[ "${RPM_MANIFEST_SHA256}" =~ ^[0-9a-f]{64}$ ]] || \
  die "invalid RPM manifest SHA256 in closure result"
[[ "$(sha256sum "${PACKAGE_ROOT}/rpms.sha256" | awk '{print $1}')" == \
  "${RPM_MANIFEST_SHA256}" ]] || die "RPM manifest SHA256 mismatch"
(
  cd "${PACKAGE_ROOT}"
  sha256sum -c rpms.sha256
) >"${EVIDENCE_DIR}/rpm-sha256-recheck.txt"
RPM_COUNT="$(find "${PACKAGE_ROOT}/rpms" -maxdepth 1 -type f -name '*.rpm' | wc -l)"
[[ "${RPM_COUNT}" -gt 0 ]] || die "RPM closure is empty"
[[ "$(wc -l <"${PACKAGE_ROOT}/rpms.sha256")" -eq "${RPM_COUNT}" ]] || \
  die "RPM manifest count does not match the directory"
[[ "$(grep -Ec 'signatures OK$' "${EVIDENCE_DIR}/rpm-signatures.txt")" -eq \
  "${RPM_COUNT}" ]] || die "RPM signature evidence count mismatch"

ASSET_DIGESTS="${BUNDLE_ROOT}/roles/app/manifests/asset-digests.tsv"
PACKAGE_DIGESTS="${BUNDLE_ROOT}/roles/app/manifests/packages.sha256"
REPO_SNAPSHOT="${BUNDLE_ROOT}/roles/app/manifests/repo-snapshot.txt"
MYSQL_ARCHIVE_NAME="mysql-5.6.51-linux-glibc2.12-x86_64.tar.gz"

grep -Fqx "$(printf '%s\t%s\t%s\t%s' \
  "${MYSQL_ARCHIVE_NAME}" "${MYSQL_SHA256}" "LOCKED" \
  "Q-050 App01 archive and required client members verified")" \
  "${ASSET_DIGESTS}" || die "MySQL asset digest is not locked to Q-050 evidence"
grep -Fqx "$(printf '%s\t%s\t%s\t%s' \
  "rpms.sha256" "${RPM_MANIFEST_SHA256}" "LOCKED" \
  "Q-050 43-RPM install-only compatible openEuler SP1 closure")" \
  "${ASSET_DIGESTS}" || die "RPM asset digest is not locked to Q-050 evidence"
grep -Fqx "${MYSQL_SHA256}  packages/${MYSQL_ARCHIVE_NAME}" \
  "${PACKAGE_DIGESTS}" || die "MySQL package digest is not locked"
grep -Fqx "${RPM_MANIFEST_SHA256}  packages/rpms.sha256" \
  "${PACKAGE_DIGESTS}" || die "RPM manifest digest is not locked"
grep -Fxq 'status=LOCKED_Q050' "${REPO_SNAPSHOT}" || \
  die "repository snapshot is not locked for Q-050"
grep -Fxq "rpm_count=${RPM_COUNT}" "${REPO_SNAPSHOT}" || \
  die "repository snapshot RPM count mismatch"
grep -Fxq "rpm_manifest_sha256=${RPM_MANIFEST_SHA256}" "${REPO_SNAPSHOT}" || \
  die "repository snapshot RPM manifest SHA256 mismatch"
grep -Fxq 'rpm_sha256_check=PASS' "${REPO_SNAPSHOT}" || \
  die "repository snapshot SHA256 gate is not PASS"
grep -Fxq 'rpm_signature_check=PASS' "${REPO_SNAPSHOT}" || \
  die "repository snapshot signature gate is not PASS"
grep -Fxq 'offline_dependency_resolution=PASS_ASSUMENO' "${REPO_SNAPSHOT}" || \
  die "repository snapshot offline dependency gate is not PASS"
grep -Fxq 'dnf_transaction_actions=INSTALL_ONLY' "${REPO_SNAPSHOT}" || \
  die "repository snapshot transaction actions are not install-only"
grep -Fxq 'install_execution=NOT_PERFORMED' "${REPO_SNAPSHOT}" || \
  die "repository snapshot records an installation"

if find "${PACKAGE_ROOT}" -type f -name '*.part' -print -quit | grep -q .; then
  die "partial download file remains in the package root"
fi
for archive in \
  "${PACKAGE_ROOT}/lnmp2.2-from-git-20260706.tar.gz" \
  "${PACKAGE_ROOT}/nginx-1.26.3.tar.gz" \
  "${PACKAGE_ROOT}/php-7.3.33.tar.bz2" \
  "${PACKAGE_ROOT}/php-7.4.33.tar.bz2" \
  "${PACKAGE_ROOT}/redis-5.3.5.tgz" \
  "${PACKAGE_ROOT}/Mosquitto-0.4.0.tgz" \
  "${PACKAGE_ROOT}/mosquitto-1.6.15.tar.gz" \
  "${PACKAGE_ROOT}/redis-5.0.14.tar.gz" \
  "${PACKAGE_ROOT}/mysql-5.6.51-linux-glibc2.12-x86_64.tar.gz"; do
  assert_archive_members_safe "${archive}"
done
python3 - "${PACKAGE_ROOT}/supervisor-4.3.0-py2.py3-none-any.whl" <<'PY'
import sys
import zipfile

with zipfile.ZipFile(sys.argv[1]) as zf:
    bad = zf.testzip()
    if bad is not None:
        raise SystemExit(f"bad wheel member: {bad}")
PY

(
  cd "${EVIDENCE_DIR}"
  sha256sum -c sha256sums.txt
) >"${EVIDENCE_DIR}/baseline-sha256-recheck.txt"

[[ ! -e "${SAFE_DIR}" ]] || die "safe evidence directory already exists: ${SAFE_DIR}"
install -d -m 0700 "${SAFE_DIR}"
cp -p \
  "${EVIDENCE_DIR}/result.env" \
  "${EVIDENCE_DIR}/framework-fb48b4d4-refresh-result.env" \
  "${EVIDENCE_DIR}/source-assets-result.env" \
  "${EVIDENCE_DIR}/source-assets-inventory.tsv" \
  "${EVIDENCE_DIR}/source-assets.sha256" \
  "${EVIDENCE_DIR}/rpm-closure-result.env" \
  "${EVIDENCE_DIR}/rpm-nevra.tsv" \
  "${EVIDENCE_DIR}/rpm-signatures.txt" \
  "${EVIDENCE_DIR}/dnf-offline-closure-assumeno.log" \
  "${EVIDENCE_DIR}/locked-source-sha256-check.txt" \
  "${EVIDENCE_DIR}/rpm-sha256-recheck.txt" \
  "${EVIDENCE_DIR}/baseline-sha256-recheck.txt" \
  "${PACKAGE_ROOT}/rpms.sha256" \
  "${SAFE_DIR}/"

ss -lntup >"${SAFE_DIR}/listeners-after.txt"
systemctl --failed --all --no-pager >"${SAFE_DIR}/failed-units-after.txt"
: >"${SAFE_DIR}/target-units-after.tsv"
for unit in "${TARGET_UNITS[@]}"; do
  enabled=$(systemctl is-enabled "${unit}" 2>/dev/null || true)
  active=$(systemctl is-active "${unit}" 2>/dev/null || true)
  printf '%s\t%s\t%s\n' "${unit}" "${enabled:-not-found}" "${active:-unknown}" \
    >>"${SAFE_DIR}/target-units-after.tsv"
  [[ "${active}" != "active" && "${active}" != "activating" ]] || \
    die "target unit is active: ${unit}"
done

TARGET_LISTENERS="$(ss -lntupH | grep -E "${TARGET_PORTS_RE}" || true)"
[[ -z "${TARGET_LISTENERS}" ]] || die "App target listener exists"
PHP_SOCKETS="$(find /run -maxdepth 3 -type s -name '*php*fpm*.sock' -print 2>/dev/null || true)"
[[ -z "${PHP_SOCKETS}" ]] || die "PHP-FPM socket exists"
TARGET_PROCESSES="$(ps -eo pid,ppid,user,group,cmd | \
  grep -Ei '[n]ginx|[p]hp-fpm|[s]upervisord|[m]osquitto|[r]edis-server|[m]ysqld' || true)"
printf '%s\n' "${TARGET_PROCESSES}" >"${SAFE_DIR}/target-processes-after.txt"
[[ -z "${TARGET_PROCESSES}" ]] || die "target process exists before Q-051"

: >"${SAFE_DIR}/formal-runtime-paths-after.tsv"
for path in \
  /usr/local/nginx \
  /usr/local/php7.3 \
  /usr/local/php7.4 \
  /opt/redis-client \
  /opt/mysql-client \
  /etc/supervisord.conf \
  /etc/cron.d/zhct-sanquan; do
  if [[ -e "${path}" || -L "${path}" ]]; then
    printf 'PRESENT\t%s\n' "${path}" >>"${SAFE_DIR}/formal-runtime-paths-after.tsv"
    die "formal runtime path exists before Q-051: ${path}"
  fi
  printf 'ABSENT\t%s\n' "${path}" >>"${SAFE_DIR}/formal-runtime-paths-after.tsv"
done

crontab -l >"${SAFE_DIR}/root-crontab-after.txt" 2>&1 || true
sed '1,2d' "${EVIDENCE_DIR}/baseline/root-crontab.txt" \
  >"${SAFE_DIR}/root-crontab-before-body.txt"
cmp -s "${SAFE_DIR}/root-crontab-before-body.txt" "${SAFE_DIR}/root-crontab-after.txt" || \
  die "root crontab changed during Q-050"

dnf history list >"${SAFE_DIR}/dnf-history-after.txt" 2>&1
for history_side in before after; do
  if [[ "${history_side}" == "before" ]]; then
    history_file="${EVIDENCE_DIR}/baseline/dnf-history.txt"
  else
    history_file="${SAFE_DIR}/dnf-history-after.txt"
  fi
  awk -F'|' '
    $1 ~ /^[[:space:]]*[0-9]+[[:space:]]*$/ {
      for (i = 1; i <= NF; i++) {
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
      }
      print $1 "|" $3 "|" $4
    }
  ' "${history_file}" >"${SAFE_DIR}/dnf-history-${history_side}-transactions.tsv"
done
[[ -s "${SAFE_DIR}/dnf-history-before-transactions.tsv" ]] || \
  die "baseline DNF history transaction set is empty"
cmp -s \
  "${SAFE_DIR}/dnf-history-before-transactions.tsv" \
  "${SAFE_DIR}/dnf-history-after-transactions.tsv" || \
  die "DNF history changed during Q-050"

FREE_BYTES="$(df -B1 --output=avail / | awk 'NR == 2 {print $1}')"
[[ "${FREE_BYTES}" -ge "${MIN_FREE_BYTES}" ]] || \
  die "root free space is below the 25GiB Q-051 entry gate"
{
  printf 'captured_at=%s\n' "$(date --iso-8601=seconds)"
  printf 'free_bytes=%s\n' "${FREE_BYTES}"
  printf 'minimum_free_bytes=%s\n' "${MIN_FREE_BYTES}"
  printf 'q051_space_gate=PASS\n'
  printf 'package_root_bytes=%s\n' "$(du -sb "${PACKAGE_ROOT}" | awk '{print $1}')"
  printf 'backup_batch_bytes=%s\n' "$(du -sb "${EVIDENCE_DIR}" | awk '{print $1}')"
} >"${SAFE_DIR}/space-gate.env"

"${BUNDLE_ROOT}/common/scripts/install-role.sh" \
  --role app --bundle-root "${BUNDLE_ROOT}" --dry-run \
  >"${SAFE_DIR}/app-install-dry-run.txt" 2>&1
grep -Fq 'install completed for role=app dry_run=1' \
  "${SAFE_DIR}/app-install-dry-run.txt" || die "App install dry-run did not complete"

{
  printf 'run_id=%s\n' "${RUN_ID}"
  printf 'completed_at=%s\n' "$(date --iso-8601=seconds)"
  printf 'v1_target_identity=PASS\n'
  printf 'v2_baseline_and_backup_integrity=PASS\n'
  printf 'v3_framework_integrity=PASS\n'
  printf 'v4_locked_source_assets=PASS\n'
  printf 'v5_mysql_client_archive=PASS\n'
  printf 'v6_rpm_closure_and_signatures=PASS\n'
  printf 'v7_offline_dnf_install_only=PASS\n'
  printf 'v8_target_quiescence=PASS\n'
  printf 'v9_space_gate=PASS\n'
  printf 'v10_safe_evidence_and_dry_run=PASS\n'
  printf 'mysql_archive_sha256=%s\n' "${MYSQL_SHA256}"
  printf 'rpm_manifest_sha256=%s\n' "${RPM_MANIFEST_SHA256}"
  printf 'rpm_count=%s\n' "${RPM_COUNT}"
  printf 'target_listener_count=0\n'
  printf 'php_socket_count=0\n'
  printf 'target_process_count=0\n'
  printf 'formal_runtime_paths=ABSENT\n'
  printf 'root_crontab_unchanged=PASS\n'
  printf 'install_execution=NOT_PERFORMED\n'
  printf 'service_start=NOT_PERFORMED\n'
  printf 'application_release=NOT_PERFORMED\n'
  printf 'manifest_lock=PASS\n'
  printf 'offline_assets_check=PASS\n'
} >"${SAFE_DIR}/offline-assets-final-result.env"

SENSITIVE_TEMP=$(mktemp /root/q050-sensitive-scan.XXXXXX)
grep -RInE \
  '(BEGIN (RSA |OPENSSH |EC )?PRIVATE KEY|password[[:space:]]*=|passwd[[:space:]]*=|token[[:space:]]*=|secret[[:space:]]*=|webhook.*https?://|mysql://[^[:space:]]+@|redis://[^[:space:]]+@)' \
  "${SAFE_DIR}" >"${SENSITIVE_TEMP}" || true
[[ ! -s "${SENSITIVE_TEMP}" ]] || die "sensitive-looking content exists in safe evidence"
printf 'sensitive_scan=PASS\n' >"${SAFE_DIR}/sensitive-scan.txt"
rm -f -- "${SENSITIVE_TEMP}"

(
  cd "${SAFE_DIR}"
  find . -type f \
    ! -name q050-final-evidence-index.sha256 \
    ! -name q050-final-evidence-index.sha256.sha256 \
    -print0 | sort -z | xargs -0 sha256sum
) >"${SAFE_DIR}/q050-final-evidence-index.sha256"
sha256sum "${SAFE_DIR}/q050-final-evidence-index.sha256" \
  >"${SAFE_DIR}/q050-final-evidence-index.sha256.sha256"
chmod -R go-rwx "${SAFE_DIR}"

tar -czf "${ARCHIVE}" -C "${EVIDENCE_DIR}" "$(basename "${SAFE_DIR}")"
sha256sum "${ARCHIVE}" >"${ARCHIVE}.sha256"
chmod 0600 "${ARCHIVE}" "${ARCHIVE}.sha256"

printf 'Q050_OFFLINE_ASSETS_CHECK_OK\n'
printf 'RUN_ID=%s\n' "${RUN_ID}"
printf 'MYSQL_ARCHIVE_SHA256=%s\n' "${MYSQL_SHA256}"
printf 'RPM_MANIFEST_SHA256=%s\n' "${RPM_MANIFEST_SHA256}"
printf 'RPM_COUNT=%s\n' "${RPM_COUNT}"
printf 'ARCHIVE=%s\n' "${ARCHIVE}"
printf 'ARCHIVE_SHA256=%s\n' "$(sha256sum "${ARCHIVE}" | awk '{print $1}')"
