#!/usr/bin/env bash

set -Eeuo pipefail
export LANG=C
umask 077

EXPECTED_HOSTNAME="sqjcmqtt01"
EXPECTED_IPV4="10.2.203.108"
EXPECTED_OS="openEuler release 24.03 (LTS-SP1)"
EXPECTED_ARCH="x86_64"
EXPECTED_EMQX_SHA256="e4cf9f507cbe32c87c4e7496b943e2d836cf1c1e4e36bc70ed67c8370fbe8484"
EXPECTED_EMQX_NEVRA="emqx-5.8.9-1.el8.x86_64"
EMQX_URL="https://github.com/emqx/emqx/releases/download/v5.8.9/emqx-5.8.9-el8-amd64.rpm"
BUNDLE_ROOT="/opt/zhct-deploy"
PACKAGE_ROOT="${BUNDLE_ROOT}/packages"
ROLE_MANIFEST_ROOT="${BUNDLE_ROOT}/roles/emqx/manifests"
EVIDENCE_ROOT="/root/q080-evidence"
BACKUP_ROOT="/var/backups/zhct-q080"
MIN_AVAILABLE_BYTES=9448928052
TARGET_PORT_RE=':(1883|8083|18083|4370|5370|8883|8084)([[:space:]]|$)'
RUN_ID=${1:-}
CONFIRM=${2:-}

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'
    "$@"
  } >"${EVIDENCE_DIR}/${name}.txt" 2>&1 || true
}

capture_shell() {
  local name=$1 command=$2
  {
    printf '# captured_at=%s\n' "$(date --iso-8601=seconds)"
    printf '# command=%s\n' "${command}"
    bash -o pipefail -c "${command}"
  } >"${EVIDENCE_DIR}/${name}.txt" 2>&1 || true
}

unit_inactive() {
  local unit=$1 enabled active
  enabled=$(systemctl is-enabled "${unit}" 2>/dev/null || true)
  active=$(systemctl is-active "${unit}" 2>/dev/null || true)
  case "${enabled}" in disabled|masked|not-found|"") ;; *) return 1 ;; esac
  case "${active}" in inactive|failed|unknown|"") ;; *) return 1 ;; esac
}

[[ "${EUID}" -eq 0 ]] || die "must run as root"
[[ "${RUN_ID}" =~ ^q080-[0-9]{8}T[0-9]{6}[+]0800$ ]] || die "invalid Q080 run id"
[[ "${CONFIRM}" == "--execute-q080-assets" ]] || die "explicit Q080 asset flag is required"
[[ "$(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"
[[ -d "${BUNDLE_ROOT}" ]] || die "bundle root is missing"
[[ -d "${ROLE_MANIFEST_ROOT}" ]] || die "EMQX role manifests are missing"
[[ ! -e "${EVIDENCE_ROOT}/${RUN_ID}" ]] || die "evidence run already exists"
[[ ! -e "${BACKUP_ROOT}/${RUN_ID}" ]] || die "backup run already exists"
[[ ! -e "${PACKAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" ]] || die "official EMQX RPM already exists"
[[ ! -e "${PACKAGE_ROOT}/rpms" ]] || die "RPM closure already exists"
[[ ! -e "${PACKAGE_ROOT}/rpms.sha256" ]] || die "RPM closure manifest already exists"
command -v curl >/dev/null || die "curl not found"
command -v dnf >/dev/null || die "dnf not found"
command -v rpm >/dev/null || die "rpm not found"
command -v sha256sum >/dev/null || die "sha256sum not found"
unit_inactive emqx.service || die "emqx.service is not disabled/inactive"
unit_inactive mosquitto.service || die "mosquitto.service is not disabled/inactive"
[[ -z "$(ss -lntupH | grep -E "${TARGET_PORT_RE}" || true)" ]] || die "an EMQX target port is occupied"
AVAILABLE_BYTES=$(df -B1 --output=avail / | awk 'NR==2 {print $1}')
[[ "${AVAILABLE_BYTES}" =~ ^[0-9]+$ ]] || die "cannot read root available bytes"
((AVAILABLE_BYTES >= MIN_AVAILABLE_BYTES)) || die "root available space is below the 20 percent gate"

EVIDENCE_DIR="${EVIDENCE_ROOT}/${RUN_ID}"
BACKUP_DIR="${BACKUP_ROOT}/${RUN_ID}"
STAGE_ROOT="${PACKAGE_ROOT}/.q080-${RUN_ID}.stage"
RPM_STAGE="${STAGE_ROOT}/rpms"
install -d -m 0700 "${EVIDENCE_DIR}" "${BACKUP_DIR}" "${RPM_STAGE}"
exec 9>/run/lock/zhct-q080-assets.lock
flock -n 9 || die "another Q080 asset process is running"

capture hostname hostnamectl
capture addresses ip -o -4 addr show scope global
capture os cat /etc/openEuler-release
capture arch uname -m
capture root-space df -B1 /
capture listeners ss -lntup
capture processes ps -eo pid,ppid,user,group,lstart,cmd --sort=pid
capture rpm-before rpm -qa --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}\t%{ARCH}\n'
capture dnf-history-before dnf history list
capture dnf-repolist dnf -v repolist --all
capture rpm-keys rpm -qa 'gpg-pubkey*'
capture firewalld firewall-cmd --list-all-zones
capture selinux getenforce
capture_shell unit-state-before "for u in emqx.service mosquitto.service; do systemctl is-enabled \"\$u\" 2>&1 || true; systemctl is-active \"\$u\" 2>&1 || true; done"
: >"${EVIDENCE_DIR}/account-state-before.tsv"
for account in emqx mosquitto; do
  user_state=ABSENT
  group_state=ABSENT
  getent passwd "${account}" >/dev/null && user_state=PRESENT
  getent group "${account}" >/dev/null && group_state=PRESENT
  printf '%s\t%s\t%s\n' "${account}" "${user_state}" "${group_state}" \
    >>"${EVIDENCE_DIR}/account-state-before.tsv"
done

PATHS=(
  /etc/emqx
  /var/lib/emqx
  /var/log/emqx
  /usr/lib/emqx
  /etc/systemd/system/emqx.service
  /usr/lib/systemd/system/emqx.service
  /etc/systemd/system/mosquitto.service
  /usr/lib/systemd/system/mosquitto.service
  /etc/mosquitto
)
: >"${EVIDENCE_DIR}/path-state-before.tsv"
BACKUP_PATHS=()
for path in "${PATHS[@]}"; do
  if [[ -e "${path}" || -L "${path}" ]]; then
    printf '%s\tPRESENT\n' "${path}" >>"${EVIDENCE_DIR}/path-state-before.tsv"
    BACKUP_PATHS+=("${path#/}")
  else
    printf '%s\tABSENT\n' "${path}" >>"${EVIDENCE_DIR}/path-state-before.tsv"
  fi
done
if ((${#BACKUP_PATHS[@]})); then
  tar --acls --xattrs --numeric-owner -czpf "${BACKUP_DIR}/system-config-before-q080.tar.gz" -C / "${BACKUP_PATHS[@]}"
fi
find "${ROLE_MANIFEST_ROOT}" -maxdepth 1 -type f -print0 | sort -z |
  xargs -0 sha256sum >"${EVIDENCE_DIR}/role-manifests-before.sha256"

dnf_history_before=$(dnf history list | awk '$1 ~ /^[0-9]+$/ {print $1; exit}')
dnf_history_before=${dnf_history_before:-0}
curl --fail --location --proto '=https' --tlsv1.2 --retry 2 \
  --output "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" "${EMQX_URL}" \
  >"${EVIDENCE_DIR}/emqx-download.log" 2>&1
actual_sha=$(sha256sum "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" | awk '{print $1}')
[[ "${actual_sha}" == "${EXPECTED_EMQX_SHA256}" ]] || die "official EMQX RPM SHA256 mismatch"
rpm --define '_pkgverify_level signature' -K "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" \
  >"${EVIDENCE_DIR}/emqx-rpm-signature.txt" 2>&1
grep -Eq 'signatures OK$|digests signatures OK$' "${EVIDENCE_DIR}/emqx-rpm-signature.txt" ||
  die "official EMQX RPM signature verification failed"
rpm -qpi "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" >"${EVIDENCE_DIR}/emqx-rpm-info.txt"
rpm -qpR "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" >"${EVIDENCE_DIR}/emqx-rpm-requires.txt"
actual_nevra=$(rpm -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm")
[[ "${actual_nevra}" == "${EXPECTED_EMQX_NEVRA}" ]] || die "official EMQX RPM NEVRA mismatch"

dnf -y install --downloadonly --nobest \
  --downloaddir="${RPM_STAGE}" \
  --setopt=strict=True \
  --setopt=install_weak_deps=False \
  --setopt=localpkg_gpgcheck=False \
  libatomic mosquitto >"${EVIDENCE_DIR}/dnf-rpm-closure-download.log" 2>&1
RPM_COUNT=$(find "${RPM_STAGE}" -maxdepth 1 -type f -name '*.rpm' | wc -l)
((RPM_COUNT > 0)) || die "DNF did not download an RPM closure"
find "${RPM_STAGE}" -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$|digests signatures OK$' "${EVIDENCE_DIR}/rpm-signatures.txt")" -eq "${RPM_COUNT}" ]] ||
  die "not every closure RPM has a trusted signature"
grep -Eiq 'BAD|NOT OK|NOKEY|NOTTRUSTED' "${EVIDENCE_DIR}/rpm-signatures.txt" &&
  die "an RPM signature is not trusted"
find "${RPM_STAGE}" -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z |
  xargs -0 rpm -qp --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}\t%{ARCH}\t%{SIZE}\t%{VENDOR}\n' |
  sort -u >"${EVIDENCE_DIR}/rpm-nevra.tsv"
awk -F'\t' '$1 == "libatomic" {found=1} END {exit !found}' "${EVIDENCE_DIR}/rpm-nevra.tsv" ||
  die "closure does not contain libatomic"
awk -F'\t' '$1 == "mosquitto" {found=1} END {exit !found}' "${EVIDENCE_DIR}/rpm-nevra.tsv" ||
  die "closure does not contain mosquitto"
mosquitto_rpm=
while IFS= read -r -d '' candidate; do
  if [[ "$(rpm -qp --qf '%{NAME}' "${candidate}")" == "mosquitto" ]]; then
    mosquitto_rpm=${candidate}
    break
  fi
done < <(find "${RPM_STAGE}" -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z)
[[ -n "${mosquitto_rpm}" ]] || die "cannot resolve the downloaded mosquitto RPM"
mosquitto_version=$(rpm -qp --qf '%{VERSION}-%{RELEASE}' "${mosquitto_rpm}")

mapfile -d '' closure_rpms < <(find "${RPM_STAGE}" -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z)
set +e
dnf --disablerepo='*' --setopt=strict=True --setopt=install_weak_deps=False \
  --setopt=localpkg_gpgcheck=False --assumeno install \
  "${closure_rpms[@]}" "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" \
  >"${EVIDENCE_DIR}/dnf-offline-assumeno.log" 2>&1
dnf_rc=$?
set -e
[[ "${dnf_rc}" -eq 1 ]] || die "unexpected offline DNF assumeno exit code: ${dnf_rc}"
grep -Fq 'Transaction Summary' "${EVIDENCE_DIR}/dnf-offline-assumeno.log" ||
  die "offline DNF did not reach Transaction Summary"
grep -Eiq 'nothing provides|conflicting requests|transaction test error|protected packages' \
  "${EVIDENCE_DIR}/dnf-offline-assumeno.log" && die "offline closure has an unresolved dependency"
grep -Eiq '(^|[[:space:]])(Upgrading|Downgrading|Removing|Reinstalling|Obsoleting)(:|[[:space:]])|^[[:space:]]*(Upgrade|Downgrade|Remove|Reinstall|Obsolete)[[:space:]]+[0-9]+' \
  "${EVIDENCE_DIR}/dnf-offline-assumeno.log" && die "offline closure contains a non-install action"

mv "${STAGE_ROOT}/emqx-5.8.9-el8-amd64.rpm" "${PACKAGE_ROOT}/"
mv "${RPM_STAGE}" "${PACKAGE_ROOT}/rpms"
rmdir "${STAGE_ROOT}"
(
  cd "${PACKAGE_ROOT}"
  find rpms -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z | xargs -0 sha256sum
) >"${PACKAGE_ROOT}/rpms.sha256"
(
  cd "${PACKAGE_ROOT}"
  sha256sum -c rpms.sha256
) >"${EVIDENCE_DIR}/rpm-sha256-check.txt"
rpm_manifest_sha=$(sha256sum "${PACKAGE_ROOT}/rpms.sha256" | awk '{print $1}')

cat >"${ROLE_MANIFEST_ROOT}/asset-digests.tsv" <<EOF
artifact	sha256	status	source
emqx-5.8.9-el8-amd64.rpm	${EXPECTED_EMQX_SHA256}	LOCKED	Q-022 official release checksum
rpms.sha256	${rpm_manifest_sha}	LOCKED	Q-080 target-resolved openEuler SP1 closure
EOF
cat >"${ROLE_MANIFEST_ROOT}/packages.sha256" <<EOF
${EXPECTED_EMQX_SHA256}  packages/emqx-5.8.9-el8-amd64.rpm
${rpm_manifest_sha}  packages/rpms.sha256
EOF
cat >"${ROLE_MANIFEST_ROOT}/repo-snapshot.txt" <<EOF
status=LOCKED_Q080
run_id=${RUN_ID}
os=openEuler-24.03-LTS-SP1-x86_64
rpm_count=${RPM_COUNT}
rpm_manifest_sha256=${rpm_manifest_sha}
offline_assumeno=PASS_INSTALL_ONLY
network_rule=download-on-emqx01-then-disablerepo-verify
evidence_dir=${EVIDENCE_DIR}
EOF
packages_temp=$(mktemp "${ROLE_MANIFEST_ROOT}/.packages.tsv.XXXXXX")
awk -F'\t' -v OFS='\t' -v version="${mosquitto_version}" -v verified="$(date +%Y-%m-%d)" '
  NR == 1 {print; next}
  $2 == "mosquitto-clients" {$3=version; $12="Q-080 target-resolved closure"; $13=verified}
  $2 == "openeuler-runtime" {$12="Q-080 target-resolved closure"; $13=verified}
  {print}
' "${ROLE_MANIFEST_ROOT}/packages.tsv" >"${packages_temp}"
mv "${packages_temp}" "${ROLE_MANIFEST_ROOT}/packages.tsv"
"${BUNDLE_ROOT}/bundle-sha256.sh" --write
"${BUNDLE_ROOT}/bundle-sha256.sh" --check >"${EVIDENCE_DIR}/bundle-sha256-check.txt"
"${BUNDLE_ROOT}/validate-assets.sh" >"${EVIDENCE_DIR}/validate-assets.txt"
grep -Fq 'PASS: 5 roles, shell syntax, manifests, dry-run and secret scan' \
  "${EVIDENCE_DIR}/validate-assets.txt" ||
  die "bundle static validation did not pass"
dnf_history_after=$(dnf history list | awk '$1 ~ /^[0-9]+$/ {print $1; exit}')
dnf_history_after=${dnf_history_after:-0}
[[ "${dnf_history_after}" == "${dnf_history_before}" ]] || die "DNF history changed during Q080"

{
  printf 'run_id=%s\n' "${RUN_ID}"
  printf 'target_identity=PASS\n'
  printf 'baseline_backup=PASS\n'
  printf 'emqx_sha256=%s\n' "${EXPECTED_EMQX_SHA256}"
  printf 'emqx_nevra=%s\n' "${EXPECTED_EMQX_NEVRA}"
  printf 'rpm_count=%s\n' "${RPM_COUNT}"
  printf 'rpm_manifest_sha256=%s\n' "${rpm_manifest_sha}"
  printf 'mosquitto_version=%s\n' "${mosquitto_version}"
  printf 'libatomic_closure=PASS\n'
  printf 'mosquitto_client_closure=PASS\n'
  printf 'offline_assumeno=PASS_INSTALL_ONLY\n'
  printf 'asset_lock=PASS\n'
  printf 'dnf_history_unchanged=PASS\n'
  printf 'install_execution=NOT_PERFORMED\n'
  printf 'service_start=NOT_PERFORMED\n'
} >"${EVIDENCE_DIR}/q080-result.env"
find "${EVIDENCE_DIR}" -type f ! -name evidence.sha256 -print0 | sort -z |
  xargs -0 sha256sum >"${EVIDENCE_DIR}/evidence.sha256"
chmod -R go-rwx "${EVIDENCE_DIR}" "${BACKUP_DIR}" "${PACKAGE_ROOT}/rpms" "${PACKAGE_ROOT}/rpms.sha256"
printf 'EMQX01_Q080_ASSETS_OK\n'
cat "${EVIDENCE_DIR}/q080-result.env"
