#!/usr/bin/env bash

set -Eeuo pipefail
export LANG=C
umask 077

EXPECTED_HOSTNAME="sqjcDB01"
EXPECTED_IPV4="10.2.208.106"
EXPECTED_OS="openEuler release 24.03 (LTS-SP1)"
EXPECTED_ARCH="x86_64"
BACKUP_ROOT="/var/backups/zhct-q060"
PACKAGE_ROOT="/opt/zhct-deploy/packages"
STAGE="${PACKAGE_ROOT}/.q060-download-stage"
MYSQL56="mysql-5.6.51-linux-glibc2.12-x86_64.tar.gz"
MYSQL80="mysql-8.0.46-linux-glibc2.28-x86_64.tar.xz"
MYSQL56_FPR="A4A9406876FCBD3C456770C88C718D3B5072E1F5"
MYSQL56_KEY_ID="8C718D3B5072E1F5"
MYSQL56_SIGNATURE_EPOCH="1609864482"
MYSQL56_KEY_EXPIRY_EPOCH="1645052400"
MYSQL80_FPR="BCA43417C3B485DD128EC6D4B7B3B788A8D3785C"
MYSQL80_KEY_ID="B7B3B788A8D3785C"

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

require_command() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

key_fingerprint() {
  local key_file="$1"
  GNUPGHOME="${GNUPG_HOME}" gpg --batch --with-colons \
    --import-options show-only --import "${key_file}" |
    awk -F: '$1 == "fpr" {print $10; exit}'
}

reject_bad_gpg_status() {
  local status_file="$1"
  ! grep -Eq '^[[]GNUPG:[]] (BADSIG|ERRSIG|REVKEYSIG|NO_PUBKEY)( |$)' \
    "${status_file}" || die "GPG reported a rejected signature state: ${status_file}"
}

verify_mysql56_historical_signature() {
  local archive="$1"
  local signature="$2"
  local status_file="$3"
  set +e
  GNUPGHOME="${GNUPG_HOME}" gpg --batch --status-fd 1 \
    --verify "${signature}" "${archive}" >"${status_file}" 2>&1
  local rc=$?
  set -e
  [[ "${rc}" -eq 0 ]] || die "MySQL 5.6 GPG verification failed"
  reject_bad_gpg_status "${status_file}"
  grep -Eq "^[[]GNUPG:[]] VALIDSIG ${MYSQL56_FPR} [^ ]+ ${MYSQL56_SIGNATURE_EPOCH} " \
    "${status_file}" || die "MySQL 5.6 signature fingerprint/time mismatch"
  grep -Eq "^[[]GNUPG:[]] EXPKEYSIG ${MYSQL56_KEY_ID} " "${status_file}" || \
    die "MySQL 5.6 did not report the pinned historical expired key"
  grep -Fxq "[GNUPG:] KEYEXPIRED ${MYSQL56_KEY_EXPIRY_EPOCH}" "${status_file}" || \
    die "MySQL 5.6 key expiry timestamp mismatch"
  [[ "${MYSQL56_SIGNATURE_EPOCH}" -lt "${MYSQL56_KEY_EXPIRY_EPOCH}" ]] || \
    die "MySQL 5.6 signature was not created before key expiry"
}

verify_mysql80_signature() {
  local archive="$1"
  local signature="$2"
  local status_file="$3"
  set +e
  GNUPGHOME="${GNUPG_HOME}" gpg --batch --status-fd 1 \
    --verify "${signature}" "${archive}" >"${status_file}" 2>&1
  local rc=$?
  set -e
  [[ "${rc}" -eq 0 ]] || die "MySQL 8.0 GPG verification failed"
  reject_bad_gpg_status "${status_file}"
  grep -Eq "^[[]GNUPG:[]] VALIDSIG ${MYSQL80_FPR} " "${status_file}" || \
    die "MySQL 8.0 signature fingerprint mismatch"
  grep -Eq "^[[]GNUPG:[]] GOODSIG ${MYSQL80_KEY_ID} " "${status_file}" || \
    die "MySQL 8.0 did not report GOODSIG for the pinned key"
}

assert_archive_layout() {
  local archive="$1"
  local top="$2"
  local members="$3"
  python3 - "${archive}" "${top}" <<'PY'
import pathlib
import posixpath
import sys
import tarfile

archive = pathlib.Path(sys.argv[1])
top = sys.argv[2]
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 or not path.parts or path.parts[0] != top:
            raise SystemExit(f"unsafe or unexpected archive member: {member.name}")
        if member.issym() or member.islnk():
            target = pathlib.PurePosixPath(member.linkname)
            if target.is_absolute():
                raise SystemExit(f"unsafe archive link: {member.name} -> {member.linkname}")
            base = path.parent if member.issym() else pathlib.PurePosixPath()
            resolved = pathlib.PurePosixPath(posixpath.normpath(str(base / target)))
            if not resolved.parts or resolved.parts[0] != top:
                raise SystemExit(f"archive link escapes top directory: {member.name} -> {member.linkname}")
PY
  tar -tf "${archive}" >"${members}"
  [[ -s "${members}" ]] || die "archive has no members: ${archive}"
  for required in bin/mysqld bin/mysql bin/mysqladmin bin/mysqldump; do
    grep -Fxq "${top}/${required}" "${members}" || \
      die "required archive member is missing: ${top}/${required}"
  done
}

[[ "${EUID}" -eq 0 ]] || die "must run as root"
[[ $# -eq 1 ]] || die "usage: $0 <q060-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}"
RESULT_FILE="${EVIDENCE_DIR}/result.env"
GNUPG_HOME="${STAGE}/gnupg-resume"

for command_name in cmp gpg sha256sum tar stat python3 awk grep; do
  require_command "${command_name}"
done
[[ "$(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"
[[ -s "${RESULT_FILE}" ]] || die "Q-060 baseline result is missing"
grep -Fxq 'baseline_result=PASS' "${RESULT_FILE}" || die "baseline gate is not PASS"
grep -Fxq 'install_execution=NOT_PERFORMED' "${RESULT_FILE}" || \
  die "baseline does not prove installation is unperformed"
[[ -d "${STAGE}" ]] || die "preserved download stage is missing"
[[ ! -e "${GNUPG_HOME}" ]] || die "resume GPG home already exists"
[[ ! -e "${EVIDENCE_DIR}/source-assets-result.env" ]] || \
  die "source asset result already exists"

for asset in \
  "${MYSQL56}" "${MYSQL56}.asc" "${MYSQL80}" "${MYSQL80}.asc" \
  mysql56-release-key.asc mysql80-release-key.asc; do
  [[ -s "${STAGE}/${asset}" ]] || die "stage asset is missing: ${asset}"
done
for final_asset in "${MYSQL56}" "${MYSQL56}.asc" "${MYSQL80}" "${MYSQL80}.asc"; do
  [[ ! -e "${PACKAGE_ROOT}/${final_asset}" ]] || \
    die "final asset already exists: ${final_asset}"
done

install -d -m 0700 "${GNUPG_HOME}"
[[ "$(key_fingerprint "${STAGE}/mysql56-release-key.asc")" == "${MYSQL56_FPR}" ]] || \
  die "MySQL 5.6 release key fingerprint mismatch"
[[ "$(key_fingerprint "${STAGE}/mysql80-release-key.asc")" == "${MYSQL80_FPR}" ]] || \
  die "MySQL 8.0 release key fingerprint mismatch"
GNUPGHOME="${GNUPG_HOME}" gpg --batch --import \
  "${STAGE}/mysql56-release-key.asc" "${STAGE}/mysql80-release-key.asc" \
  >"${EVIDENCE_DIR}/resume-mysql-gpg-key-import.txt" 2>&1

verify_mysql56_historical_signature \
  "${STAGE}/${MYSQL56}" "${STAGE}/${MYSQL56}.asc" \
  "${EVIDENCE_DIR}/resume-mysql56-gpg-status.txt"
verify_mysql80_signature \
  "${STAGE}/${MYSQL80}" "${STAGE}/${MYSQL80}.asc" \
  "${EVIDENCE_DIR}/resume-mysql80-gpg-status.txt"
assert_archive_layout "${STAGE}/${MYSQL56}" \
  "mysql-5.6.51-linux-glibc2.12-x86_64" \
  "${EVIDENCE_DIR}/mysql56-archive-members.txt"
assert_archive_layout "${STAGE}/${MYSQL80}" \
  "mysql-8.0.46-linux-glibc2.28-x86_64" \
  "${EVIDENCE_DIR}/mysql80-archive-members.txt"

for key_name in mysql56-release-key.asc mysql80-release-key.asc; do
  if [[ -e "${EVIDENCE_DIR}/${key_name}" ]]; then
    cmp -s "${STAGE}/${key_name}" "${EVIDENCE_DIR}/${key_name}" || \
      die "preserved release-key evidence differs from the stage: ${key_name}"
  else
    install -m 0600 "${STAGE}/${key_name}" "${EVIDENCE_DIR}/${key_name}"
  fi
done
if [[ -e "${EVIDENCE_DIR}/mysql-release-keys.sha256" ]]; then
  sha256sum -c "${EVIDENCE_DIR}/mysql-release-keys.sha256" \
    >"${EVIDENCE_DIR}/resume-mysql-release-keys-check.txt"
else
  sha256sum \
    "${EVIDENCE_DIR}/mysql56-release-key.asc" \
    "${EVIDENCE_DIR}/mysql80-release-key.asc" \
    >"${EVIDENCE_DIR}/mysql-release-keys.sha256"
fi
(
  cd "${STAGE}"
  sha256sum "${MYSQL56}" "${MYSQL56}.asc" "${MYSQL80}" "${MYSQL80}.asc"
) >"${EVIDENCE_DIR}/mysql-source-assets.sha256"
{
  printf 'artifact\tbytes\tsha256\tgpg_fingerprint\tstatus\n'
  printf '%s\t%s\t%s\t%s\tLOCKED_VALID_SIGNATURE_EXPIRED_PINNED_KEY\n' \
    "${MYSQL56}" "$(stat -c %s "${STAGE}/${MYSQL56}")" \
    "$(sha256sum "${STAGE}/${MYSQL56}" | awk '{print $1}')" "${MYSQL56_FPR}"
  printf '%s\t%s\t%s\t%s\tLOCKED\n' \
    "${MYSQL80}" "$(stat -c %s "${STAGE}/${MYSQL80}")" \
    "$(sha256sum "${STAGE}/${MYSQL80}" | awk '{print $1}')" "${MYSQL80_FPR}"
} >"${EVIDENCE_DIR}/mysql-source-assets.tsv"

rm -rf "${STAGE}/gnupg" "${GNUPG_HOME}"
rm -f "${STAGE}/mysql56-release-key.asc" "${STAGE}/mysql80-release-key.asc"
for final_asset in "${MYSQL56}" "${MYSQL56}.asc" "${MYSQL80}" "${MYSQL80}.asc"; do
  mv "${STAGE}/${final_asset}" "${PACKAGE_ROOT}/${final_asset}"
done
rmdir "${STAGE}"
chmod 0640 \
  "${PACKAGE_ROOT}/${MYSQL56}" "${PACKAGE_ROOT}/${MYSQL56}.asc" \
  "${PACKAGE_ROOT}/${MYSQL80}" "${PACKAGE_ROOT}/${MYSQL80}.asc"

{
  printf 'captured_at=%s\n' "$(date --iso-8601=seconds)"
  printf 'source_asset_download=PASS\n'
  printf 'mysql56_gpg=PASS_VALID_SIGNATURE_EXPIRED_PINNED_KEY\n'
  printf 'mysql56_signature_epoch=%s\n' "${MYSQL56_SIGNATURE_EPOCH}"
  printf 'mysql56_key_expiry_epoch=%s\n' "${MYSQL56_KEY_EXPIRY_EPOCH}"
  printf 'mysql80_gpg=PASS\n'
  printf 'archive_layout=PASS\n'
  printf 'manifest_update=PENDING_LOCAL_EVIDENCE_REVIEW\n'
  printf 'install_execution=NOT_PERFORMED\n'
  printf 'post_clone_execution=NOT_PERFORMED\n'
} >"${EVIDENCE_DIR}/source-assets-result.env"
chmod 0600 \
  "${EVIDENCE_DIR}/mysql56-archive-members.txt" \
  "${EVIDENCE_DIR}/mysql80-archive-members.txt" \
  "${EVIDENCE_DIR}/mysql-source-assets.sha256" \
  "${EVIDENCE_DIR}/mysql-source-assets.tsv" \
  "${EVIDENCE_DIR}"/resume-mysql-* \
  "${EVIDENCE_DIR}/source-assets-result.env"

printf 'Q060_SOURCE_ASSETS_RESUMED_LOCKED_OK\n'
cat "${EVIDENCE_DIR}/source-assets-result.env"
