#!/usr/bin/env bash
# RHCSA Exam Mode - local grader (RHEL 10 / EX200-style practice)
# Usage:
#   bash exam-grader.sh <scenario_id>
# Example:
#   bash exam-grader.sh users-sudo-ssh
#
# Output: JSON (single line) + a readable summary.
set -u

SCENARIO_ID="${1:-}"
if [[ -z "$SCENARIO_ID" ]]; then
  echo "Usage: $0 <scenario_id>" >&2
  echo "Try: $0 users-sudo-ssh" >&2
  exit 2
fi

# ---------- helpers ----------
json_escape() {
  # minimal JSON string escape
  local s="${1//\\/\\\\}"
  s="${s//\"/\\\"}"
  s="${s//$'\n'/\\n}"
  s="${s//$'\r'/}"
  printf '%s' "$s"
}

CHECKS_JSON=""
SCORE=0
MAX=0
CHECK_COUNT=0

add_check() {
  local id="$1" ok="$2" pts="$3" maxpts="$4" msg="$5"
  ((CHECK_COUNT+=1))
  ((MAX+=maxpts))
  if [[ "$ok" == "1" ]]; then
    ((SCORE+=pts))
  fi

  local ok_json="false"
  [[ "$ok" == "1" ]] && ok_json="true"

  local entry
  entry=$(printf '{"id":"%s","ok":%s,"points":%d,"max_points":%d,"message":"%s"}' \
    "$(json_escape "$id")" \
    "$ok_json" \
    "$pts" \
    "$maxpts" \
    "$(json_escape "$msg")")

  if [[ -z "$CHECKS_JSON" ]]; then
    CHECKS_JSON="$entry"
  else
    CHECKS_JSON="$CHECKS_JSON,$entry"
  fi
}

have_cmd() { command -v "$1" >/dev/null 2>&1; }

# ---------- scenario: users-sudo-ssh ----------
grade_users_sudo_ssh() {
  # user existence
  if id examuser >/dev/null 2>&1; then
    add_check "user_exists" 1 10 10 "User examuser exists"
  else
    add_check "user_exists" 0 0 10 "User examuser missing"
  fi

  # group ops + membership
  if getent group ops >/dev/null 2>&1; then
    if id -nG examuser 2>/dev/null | tr ' ' '\n' | grep -qx "ops"; then
      add_check "group_ops_membership" 1 10 10 "examuser is in group ops"
    else
      add_check "group_ops_membership" 0 0 10 "examuser not in group ops"
    fi
  else
    add_check "group_ops_exists" 0 0 10 "Group ops missing"
  fi

  # sudo rule
  local sudo_ok=0
  if [[ -d /etc/sudoers.d ]]; then
    if grep -RqsE '^\s*%ops\s+ALL=\(ALL(:ALL)?\)\s+NOPASSWD:\s*/usr/bin/systemctl\s*$' /etc/sudoers /etc/sudoers.d 2>/dev/null; then
      sudo_ok=1
    fi
  fi
  if [[ "$sudo_ok" == "1" ]]; then
    add_check "sudo_rule" 1 15 15 "Sudoers rule for ops -> /usr/bin/systemctl (NOPASSWD) found"
  else
    add_check "sudo_rule" 0 0 15 "Missing/incorrect sudoers rule. Expected: %ops ALL=(ALL) NOPASSWD: /usr/bin/systemctl"
  fi

  # ssh authorized_keys presence + perms (best effort)
  local home_dir
  home_dir="$(getent passwd examuser 2>/dev/null | cut -d: -f6)"
  if [[ -n "${home_dir:-}" && -d "$home_dir" ]]; then
    local ak="$home_dir/.ssh/authorized_keys"
    local sshd="$home_dir/.ssh"
    if [[ -f "$ak" ]]; then
      if grep -qE '^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp)' "$ak" 2>/dev/null; then
        add_check "authorized_keys_present" 1 10 10 "authorized_keys present and looks like it contains an SSH public key"
      else
        add_check "authorized_keys_present" 0 0 10 "authorized_keys exists but does not look like it contains a public key"
      fi

      # perms: .ssh not writable by group/others, authorized_keys not writable by group/others
      local ssh_mode ak_mode
      ssh_mode="$(stat -c %a "$sshd" 2>/dev/null || echo "")"
      ak_mode="$(stat -c %a "$ak" 2>/dev/null || echo "")"
      if [[ -n "$ssh_mode" && -n "$ak_mode" ]]; then
        # numeric modes; accept <= 755 and no group/other write
        local ssh_gw=$(( (10#$ssh_mode / 10) % 10 ))
        local ssh_ow=$(( 10#$ssh_mode % 10 ))
        local ak_gw=$(( (10#$ak_mode / 10) % 10 ))
        local ak_ow=$(( 10#$ak_mode % 10 ))
        if (( (ssh_gw & 2) == 0 )) && (( (ssh_ow & 2) == 0 )) && (( (ak_gw & 2) == 0 )) && (( (ak_ow & 2) == 0 )); then
          add_check "ssh_perms" 1 5 5 "Permissions are not group/other-writable"
        else
          add_check "ssh_perms" 0 0 5 "Permissions too open (group/other-writable). Recommended: ~/.ssh 700, authorized_keys 600"
        fi
      else
        add_check "ssh_perms" 0 0 5 "Could not read permissions for ~/.ssh or authorized_keys"
      fi
    else
      add_check "authorized_keys_present" 0 0 10 "authorized_keys missing: $ak"
      add_check "ssh_perms" 0 0 5 "Cannot validate permissions without authorized_keys"
    fi
  else
    add_check "authorized_keys_present" 0 0 10 "Cannot locate home directory for examuser"
    add_check "ssh_perms" 0 0 5 "Cannot validate permissions without home directory"
  fi
}

# ---------- scenario: storage-lvm-mount ----------
root_disk() {
  # try to map / to a base disk name (e.g., /dev/sda2 -> sda)
  local src
  src="$(findmnt -n -o SOURCE / 2>/dev/null || true)"
  src="${src##*/}"
  # strip partition suffix (nvme0n1p2, sda2)
  if [[ "$src" =~ ^(nvme[0-9]+n[0-9]+)p[0-9]+$ ]]; then
    echo "${BASH_REMATCH[1]}"
  else
    echo "${src%%[0-9]*}"
  fi
}

pick_extra_disk() {
  local rd
  rd="$(root_disk)"
  # list disks, exclude root disk
  lsblk -dn -o NAME,TYPE | awk '$2=="disk"{print $1}' | while read -r d; do
    [[ "$d" == "$rd" ]] && continue
    echo "/dev/$d"
    return 0
  done
  return 1
}

grade_storage_lvm_mount() {
  local disk
  disk="/dev/vdb"
  if [[ ! -b "$disk" ]]; then
    disk="$(pick_extra_disk || true)"
  fi

  if [[ -n "$disk" && -b "$disk" ]]; then
    if pvs --noheadings -o pv_name 2>/dev/null | awk '{print $1}' | grep -qx "$disk"; then
      add_check "pv" 1 10 10 "PV exists on $disk"
    else
      add_check "pv" 0 0 10 "PV not found on $disk (or PV missing)"
    fi
  else
    add_check "pv" 0 0 10 "No extra disk detected (expected /dev/vdb or another non-root disk)"
  fi

  if vgs vgdata >/dev/null 2>&1; then
    add_check "vg" 1 10 10 "VG vgdata exists"
  else
    add_check "vg" 0 0 10 "VG vgdata missing"
  fi

  if lvs vgdata/lvdata >/dev/null 2>&1; then
    add_check "lv" 1 10 10 "LV lvdata exists in vgdata"
  else
    add_check "lv" 0 0 10 "LV vgdata/lvdata missing"
  fi

  if [[ -e /dev/vgdata/lvdata ]]; then
    local fstype
    fstype="$(lsblk -no FSTYPE /dev/vgdata/lvdata 2>/dev/null | head -n1 || true)"
    if [[ "$fstype" == "xfs" ]]; then
      add_check "fs" 1 10 10 "Filesystem on /dev/vgdata/lvdata is XFS"
    else
      add_check "fs" 0 0 10 "Expected XFS on /dev/vgdata/lvdata, got: ${fstype:-unknown}"
    fi
  else
    add_check "fs" 0 0 10 "Device /dev/vgdata/lvdata not found"
  fi

  if findmnt -n /data >/dev/null 2>&1; then
    add_check "mount" 1 10 10 "/data is mounted"
  else
    add_check "mount" 0 0 10 "/data is not mounted"
  fi

  if grep -Eq '^\s*(UUID=|LABEL=)\S+\s+/data\s' /etc/fstab 2>/dev/null; then
    add_check "fstab" 1 10 10 "/etc/fstab has /data entry using UUID or LABEL"
  else
    add_check "fstab" 0 0 10 "Missing/incorrect fstab entry for /data (should use UUID=... or LABEL=...)"
  fi

  if [[ -d /data/projects ]]; then
    local mode owner group
    mode="$(stat -c %a /data/projects 2>/dev/null || echo "")"
    owner="$(stat -c %U /data/projects 2>/dev/null || echo "")"
    group="$(stat -c %G /data/projects 2>/dev/null || echo "")"
    if [[ "$owner" == "root" && "$group" == "ops" ]]; then
      add_check "projects_owner" 1 5 5 "/data/projects owned by root:ops"
    else
      add_check "projects_owner" 0 0 5 "Expected /data/projects owner root:ops, got ${owner:-?}:${group:-?}"
    fi
    if [[ "$mode" == "2775" ]]; then
      add_check "projects_mode" 1 5 5 "/data/projects mode is 2775 (setgid)"
    else
      add_check "projects_mode" 0 0 5 "Expected /data/projects mode 2775, got ${mode:-unknown}"
    fi
  else
    add_check "projects_owner" 0 0 5 "/data/projects missing"
    add_check "projects_mode" 0 0 5 "/data/projects missing"
  fi
}

# ---------- scenario: selinux-context-port ----------
grade_selinux_context_port() {
  local enf
  enf="$(getenforce 2>/dev/null || true)"
  if [[ "$enf" == "Enforcing" ]]; then
    add_check "enforcing" 1 10 10 "SELinux is Enforcing"
  else
    add_check "enforcing" 0 0 10 "SELinux not Enforcing (got: ${enf:-unknown})"
  fi

  if [[ -d /webdata && -f /webdata/index.html ]]; then
    add_check "webdata_files" 1 5 5 "/webdata and index.html exist"
  else
    add_check "webdata_files" 0 0 5 "/webdata or /webdata/index.html missing"
  fi

  if have_cmd semanage; then
    if semanage fcontext -l 2>/dev/null | grep -Eq '^/webdata\(/\.?\*\)\?\s+all files\s+system_u:object_r:httpd_sys_content_t'; then
      add_check "fcontext_persistent" 1 10 10 "Persistent fcontext for /webdata -> httpd_sys_content_t found"
    else
      # be a bit more flexible on the line format
      if semanage fcontext -l 2>/dev/null | grep -Eq '/webdata.*httpd_sys_content_t'; then
        add_check "fcontext_persistent" 1 7 10 "Found a /webdata fcontext rule with httpd_sys_content_t (format differs)"
      else
        add_check "fcontext_persistent" 0 0 10 "No persistent fcontext rule for /webdata with httpd_sys_content_t"
      fi
    fi

    # applied label
    local z
    z="$(ls -Zd /webdata 2>/dev/null | awk '{print $1}' || true)"
    if [[ "$z" == *":httpd_sys_content_t:"* ]]; then
      add_check "label_applied" 1 10 10 "Applied label on /webdata is httpd_sys_content_t"
    else
      add_check "label_applied" 0 0 10 "Label on /webdata not httpd_sys_content_t (got: ${z:-unknown})"
    fi

    # port label
    if semanage port -l 2>/dev/null | awk '$1=="http_port_t"{print $0}' | grep -Eq '(^|,| )8081(/tcp)?(,|$| )'; then
      add_check "port_8081" 1 10 10 "Port 8081 labeled http_port_t"
    else
      if semanage port -l 2>/dev/null | grep -qE '\b8081\b'; then
        add_check "port_8081" 0 0 10 "Port 8081 exists but not labeled http_port_t"
      else
        add_check "port_8081" 0 0 10 "Port 8081 not found in SELinux port labels"
      fi
    fi
  else
    add_check "fcontext_persistent" 0 0 10 "semanage missing (install policycoreutils-python-utils)"
    add_check "label_applied" 0 0 10 "Cannot validate labels without semanage"
    add_check "port_8081" 0 0 10 "Cannot validate port label without semanage"
  fi

  local bool
  bool="$(getsebool httpd_read_user_content 2>/dev/null || true)"
  if echo "$bool" | grep -qE '\bon\b'; then
    add_check "boolean" 1 10 10 "httpd_read_user_content is ON"
  else
    add_check "boolean" 0 0 10 "httpd_read_user_content is not ON"
  fi
}

# ---------- scenario: networking ----------
grade_networking_nmcli_hosts() {
  local hn
  hn="$(hostnamectl --static 2>/dev/null || hostname 2>/dev/null || true)"
  if [[ "$hn" == "rhel10-exam" ]]; then
    add_check "hostname" 1 10 10 "Hostname is rhel10-exam"
  else
    add_check "hostname" 0 0 10 "Hostname expected rhel10-exam (got: ${hn:-unknown})"
  fi

  if grep -Eq '^\s*10\.10\.10\.10\s+.*\bintranet\.local\b' /etc/hosts 2>/dev/null; then
    add_check "hosts_entry" 1 10 10 "/etc/hosts contains 10.10.10.10 intranet.local"
  else
    add_check "hosts_entry" 0 0 10 "Missing /etc/hosts entry for intranet.local"
  fi

  if have_cmd nmcli; then
    local dev con
    dev="$(ip route show default 2>/dev/null | awk '/default/{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')"
    con="$(nmcli -t -f NAME,DEVICE con show --active 2>/dev/null | awk -F: -v d="$dev" '$2==d{print $1; exit}')"
    if [[ -n "$con" ]]; then
      local method dns
      method="$(nmcli -g ipv4.method con show "$con" 2>/dev/null | head -n1 || true)"
      dns="$(nmcli -g ipv4.dns con show "$con" 2>/dev/null | head -n1 || true)"
      if [[ "$method" == "manual" ]]; then
        add_check "ipv4_manual" 1 10 10 "Connection '$con' uses ipv4.method=manual"
      else
        add_check "ipv4_manual" 0 0 10 "Expected ipv4.method=manual on '$con' (got: ${method:-unknown})"
      fi
      if [[ -n "$dns" ]]; then
        add_check "dns_configured" 1 5 5 "DNS configured via NetworkManager (ipv4.dns is set)"
      else
        add_check "dns_configured" 0 0 5 "No DNS configured in NetworkManager for '$con'"
      fi
    else
      add_check "ipv4_manual" 0 0 10 "Could not identify active NM connection for default route"
      add_check "dns_configured" 0 0 5 "Could not identify active NM connection for default route"
    fi
  else
    add_check "ipv4_manual" 0 0 10 "nmcli missing"
    add_check "dns_configured" 0 0 5 "nmcli missing"
  fi
}

# ---------- scenario: firewalld ----------
grade_firewalld_service() {
  local en ac
  en="$(systemctl is-enabled firewalld 2>/dev/null || true)"
  ac="$(systemctl is-active firewalld 2>/dev/null || true)"
  if [[ "$en" == "enabled" && "$ac" == "active" ]]; then
    add_check "firewalld_running" 1 10 10 "firewalld is enabled and active"
  else
    add_check "firewalld_running" 0 0 10 "firewalld not enabled/active (enabled=${en:-?}, active=${ac:-?})"
  fi

  if have_cmd firewall-cmd; then
    local zone
    zone="$(firewall-cmd --get-default-zone 2>/dev/null || true)"
    if [[ -n "$zone" ]]; then
      local ports_rt ports_perm
      ports_rt="$(firewall-cmd --zone="$zone" --list-ports 2>/dev/null || true)"
      ports_perm="$(firewall-cmd --permanent --zone="$zone" --list-ports 2>/dev/null || true)"
      if echo "$ports_rt" | tr ' ' '\n' | grep -qx "8081/tcp"; then
        add_check "port_runtime" 1 10 10 "8081/tcp open in runtime config (zone=$zone)"
      else
        add_check "port_runtime" 0 0 10 "8081/tcp not open in runtime config (zone=$zone)"
      fi
      if echo "$ports_perm" | tr ' ' '\n' | grep -qx "8081/tcp"; then
        add_check "port_permanent" 1 10 10 "8081/tcp open in permanent config (zone=$zone)"
      else
        add_check "port_permanent" 0 0 10 "8081/tcp not open in permanent config (zone=$zone)"
      fi
    else
      add_check "port_runtime" 0 0 10 "Could not get default zone"
      add_check "port_permanent" 0 0 10 "Could not get default zone"
    fi
  else
    add_check "port_runtime" 0 0 10 "firewall-cmd missing"
    add_check "port_permanent" 0 0 10 "firewall-cmd missing"
  fi
}

# ---------- scenario: systemd timer ----------
grade_systemd_timer() {
  if [[ -x /usr/local/bin/exam-report.sh ]]; then
    add_check "script" 1 10 10 "Script /usr/local/bin/exam-report.sh exists and is executable"
  else
    add_check "script" 0 0 10 "Missing or not executable: /usr/local/bin/exam-report.sh"
  fi

  if systemctl cat exam-report.service >/dev/null 2>&1; then
    add_check "service_unit" 1 10 10 "exam-report.service exists"
  else
    add_check "service_unit" 0 0 10 "exam-report.service missing"
  fi

  if systemctl cat exam-report.timer >/dev/null 2>&1; then
    add_check "timer_unit" 1 10 10 "exam-report.timer exists"
  else
    add_check "timer_unit" 0 0 10 "exam-report.timer missing"
  fi

  local enabled active
  enabled="$(systemctl is-enabled exam-report.timer 2>/dev/null || true)"
  active="$(systemctl is-active exam-report.timer 2>/dev/null || true)"
  if [[ "$enabled" == "enabled" ]]; then
    add_check "timer_enabled" 1 5 5 "Timer is enabled"
  else
    add_check "timer_enabled" 0 0 5 "Timer not enabled"
  fi
  if [[ "$active" == "active" ]]; then
    add_check "timer_active" 1 5 5 "Timer is active"
  else
    add_check "timer_active" 0 0 5 "Timer not active"
  fi

  # validate interval (best effort)
  local timer_txt
  timer_txt="$(systemctl cat exam-report.timer 2>/dev/null || true)"
  if echo "$timer_txt" | grep -Eq 'OnUnitActiveSec=.*(5min|300s|300sec)'; then
    add_check "timer_interval" 1 10 10 "Timer interval looks like 5 minutes (OnUnitActiveSec)"
  else
    if echo "$timer_txt" | grep -Eq 'OnCalendar=.*(0/5|5min|:0/5)'; then
      add_check "timer_interval" 1 7 10 "Timer uses OnCalendar that looks like 5-minute cadence"
    else
      add_check "timer_interval" 0 0 10 "Could not confirm 5-minute cadence in timer unit"
    fi
  fi
}

# ---------- scenario: journald persist ----------
parse_journal_size_mb() {
  # returns integer MB (best effort)
  local out num unit
  out="$(journalctl --disk-usage 2>/dev/null | tr -s ' ')"
  # "Archived and active journals take up 128.0M in the file system."
  num="$(echo "$out" | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9.]+[KMG]$/){print $i; exit}}')"
  if [[ -z "$num" ]]; then
    echo ""
    return
  fi
  unit="${num: -1}"
  num="${num%?}"
  # convert to MB
  awk -v n="$num" -v u="$unit" 'BEGIN{
    if(u=="K") printf "%d", (n/1024);
    else if(u=="M") printf "%d", n;
    else if(u=="G") printf "%d", (n*1024);
    else printf "";
  }'
}

grade_journald_persist() {
  if grep -Eq '^\s*Storage\s*=\s*persistent\s*$' /etc/systemd/journald.conf 2>/dev/null; then
    add_check "storage_persistent" 1 15 15 "journald Storage=persistent configured"
  else
    add_check "storage_persistent" 0 0 15 "Storage=persistent not set in /etc/systemd/journald.conf (or commented)"
  fi

  if [[ -d /var/log/journal ]]; then
    add_check "journal_dir" 1 10 10 "/var/log/journal exists"
  else
    add_check "journal_dir" 0 0 10 "/var/log/journal missing"
  fi

  if have_cmd journalctl; then
    local mb
    mb="$(parse_journal_size_mb)"
    if [[ -n "$mb" ]]; then
      if (( mb <= 220 )); then
        add_check "disk_usage" 1 10 10 "Journal disk usage is reasonably small (~${mb}M)"
      else
        add_check "disk_usage" 0 0 10 "Journal disk usage seems high (~${mb}M). Did you vacuum to ~200M?"
      fi
    else
      add_check "disk_usage" 0 0 10 "Could not parse journalctl --disk-usage output"
    fi
  else
    add_check "disk_usage" 0 0 10 "journalctl missing"
  fi
}

# ---------- scenario: time sync client (chrony) ----------
grade_time_sync_client() {
  if rpm -q chrony >/dev/null 2>&1; then
    add_check "chrony_installed" 1 10 10 "chrony installed"
  else
    add_check "chrony_installed" 0 0 10 "chrony not installed"
  fi

  if grep -Eq '^\s*(server|pool)\s+10\.10\.10\.20(\s|$)' /etc/chrony.conf 2>/dev/null; then
    add_check "chrony_config" 1 15 15 "chrony configured for 10.10.10.20"
  else
    add_check "chrony_config" 0 0 15 "Missing chrony server 10.10.10.20 in /etc/chrony.conf"
  fi

  local en ac
  en="$(systemctl is-enabled chronyd 2>/dev/null || true)"
  ac="$(systemctl is-active chronyd 2>/dev/null || true)"
  if [[ "$en" == "enabled" && "$ac" == "active" ]]; then
    add_check "chronyd_running" 1 10 10 "chronyd enabled and active"
  else
    add_check "chronyd_running" 0 0 10 "chronyd not enabled/active (enabled=${en:-?}, active=${ac:-?})"
  fi

  if have_cmd chronyc; then
    if chronyc sources -v 2>/dev/null | grep -q '10\.10\.10\.20'; then
      add_check "chrony_sources" 1 10 10 "chronyc sees 10.10.10.20 in sources"
    else
      add_check "chrony_sources" 0 0 10 "chronyc does not show 10.10.10.20 (server may be unreachable in your lab)"
    fi
  else
    add_check "chrony_sources" 0 0 10 "chronyc missing"
  fi
}



# ---------- scenario: webstack-httpd-8081 ----------
grade_webstack_httpd_8081() {
  # httpd installed
  if rpm -q httpd >/dev/null 2>&1; then
    add_check "httpd_pkg" 1 10 10 "httpd installed"
  else
    add_check "httpd_pkg" 0 0 10 "httpd not installed"
  fi

  # service enabled + active
  if systemctl is-enabled httpd >/dev/null 2>&1; then
    add_check "httpd_enabled" 1 10 10 "httpd enabled"
  else
    add_check "httpd_enabled" 0 0 10 "httpd not enabled"
  fi
  if systemctl is-active httpd >/dev/null 2>&1; then
    add_check "httpd_active" 1 10 10 "httpd active"
  else
    add_check "httpd_active" 0 0 10 "httpd not active"
  fi

  # Listen 8081 somewhere
  if grep -RqsE '^[[:space:]]*Listen[[:space:]]+8081([[:space:]]|$)' /etc/httpd 2>/dev/null; then
    add_check "httpd_listen_8081" 1 10 10 "httpd configured to listen on 8081"
  else
    add_check "httpd_listen_8081" 0 0 10 "Missing 'Listen 8081' in /etc/httpd/*"
  fi

  # DocumentRoot /webdata (best effort)
  if grep -RqsE '^[[:space:]]*DocumentRoot[[:space:]]+"/webdata"' /etc/httpd 2>/dev/null; then
    add_check "docroot_webdata" 1 10 10 "DocumentRoot set to /webdata"
  else
    add_check "docroot_webdata" 0 0 10 "DocumentRoot not set to /webdata (best effort check)"
  fi

  # content check (bonus-like but counted)
  if [[ -f /webdata/index.html ]] && grep -qs "RHCSA_EXAM_OK" /webdata/index.html; then
    add_check "index_content" 1 10 10 "index.html contains RHCSA_EXAM_OK"
  else
    add_check "index_content" 0 0 10 "Missing /webdata/index.html or RHCSA_EXAM_OK string"
  fi

  # SELinux enforcing
  if have_cmd getenforce && [[ "$(getenforce 2>/dev/null)" == "Enforcing" ]]; then
    add_check "selinux_enforcing" 1 5 5 "SELinux enforcing"
  else
    add_check "selinux_enforcing" 0 0 5 "SELinux not enforcing"
  fi

  # fcontext persistent + label applied
  if have_cmd semanage && semanage fcontext -l 2>/dev/null | grep -qE '^/webdata(\(/.*\)\?)?[[:space:]]'; then
    add_check "fcontext_webdata" 1 10 10 "Persistent fcontext exists for /webdata"
  else
    add_check "fcontext_webdata" 0 0 10 "Missing persistent fcontext for /webdata"
  fi
  if [[ -e /webdata/index.html ]]; then
    local ctx
    ctx="$(ls -Z /webdata/index.html 2>/dev/null | awk '{print $1}' || true)"
    if [[ "$ctx" == *":httpd_sys_content_t:"* ]] || [[ "$ctx" == *":httpd_sys_content_t" ]]; then
      add_check "label_applied" 1 10 10 "SELinux label applied (httpd_sys_content_t)"
    else
      add_check "label_applied" 0 0 10 "SELinux label not applied to /webdata/index.html (expected httpd_sys_content_t)"
    fi
  else
    add_check "label_applied" 0 0 10 "/webdata/index.html missing for label check"
  fi

  # port label
  if have_cmd semanage && semanage port -l 2>/dev/null | grep -qE '^http_port_t[[:space:]]+tcp[[:space:]].*\b8081\b'; then
    add_check "port_8081" 1 10 10 "8081 labeled as http_port_t"
  else
    add_check "port_8081" 0 0 10 "8081 not labeled as http_port_t"
  fi

  # firewall port 8081 permanent + runtime
  if have_cmd firewall-cmd; then
    local zone
    zone="$(firewall-cmd --get-default-zone 2>/dev/null || echo public)"
    if firewall-cmd --permanent --zone="$zone" --list-ports 2>/dev/null | tr ' ' '\n' | grep -qx "8081/tcp"; then
      add_check "fw_perm_8081" 1 10 10 "8081/tcp in permanent firewall rules"
    else
      add_check "fw_perm_8081" 0 0 10 "8081/tcp missing in permanent firewall rules"
    fi
    if firewall-cmd --zone="$zone" --list-ports 2>/dev/null | tr ' ' '\n' | grep -qx "8081/tcp"; then
      add_check "fw_runtime_8081" 1 5 5 "8081/tcp in runtime firewall rules"
    else
      add_check "fw_runtime_8081" 0 0 5 "8081/tcp missing in runtime firewall rules"
    fi
  else
    add_check "firewalld_cmd" 0 0 5 "firewall-cmd not found"
  fi

  # curl test (best effort)
  if have_cmd curl; then
    if curl -fsS --max-time 2 "http://127.0.0.1:8081/" 2>/dev/null | grep -q "RHCSA_EXAM_OK"; then
      add_check "curl_local" 1 5 5 "curl localhost:8081 returns expected content"
    else
      add_check "curl_local" 0 0 5 "curl localhost:8081 did not return expected content"
    fi
  else
    add_check "curl_local" 0 0 5 "curl not available"
  fi
}

# ---------- scenario: storage-lvm-acl-backup ----------
grade_storage_lvm_acl_backup() {
  local disk
  disk="/dev/vdb"
  if [[ ! -b "$disk" ]]; then
    disk="$(pick_extra_disk || true)"
  fi

  if [[ -n "$disk" && -b "$disk" ]]; then
    if pvs --noheadings -o pv_name 2>/dev/null | awk '{print $1}' | grep -qx "$disk"; then
      add_check "pv" 1 5 5 "PV exists on $disk"
    else
      add_check "pv" 0 0 5 "PV not found on $disk"
    fi
  else
    add_check "pv" 0 0 5 "No extra disk detected"
  fi

  if vgs vgshare >/dev/null 2>&1; then
    add_check "vg" 1 10 10 "VG vgshare exists"
  else
    add_check "vg" 0 0 10 "VG vgshare missing"
  fi

  if lvs vgshare/lvshare >/dev/null 2>&1; then
    add_check "lv" 1 10 10 "LV vgshare/lvshare exists"
  else
    add_check "lv" 0 0 10 "LV vgshare/lvshare missing"
  fi

  if [[ -e /dev/vgshare/lvshare ]]; then
    local fstype
    fstype="$(lsblk -no FSTYPE /dev/vgshare/lvshare 2>/dev/null | head -n1 || true)"
    if [[ "$fstype" == "xfs" ]]; then
      add_check "fs" 1 10 10 "Filesystem is XFS"
    else
      add_check "fs" 0 0 10 "Expected XFS, got: ${fstype:-unknown}"
    fi
  else
    add_check "fs" 0 0 10 "Device /dev/vgshare/lvshare not found"
  fi

  if findmnt -n /srv/share >/dev/null 2>&1; then
    add_check "mount" 1 10 10 "/srv/share is mounted"
  else
    add_check "mount" 0 0 10 "/srv/share is not mounted"
  fi

  if grep -Eq '^\s*(UUID=|LABEL=)\S+\s+/srv/share\s' /etc/fstab 2>/dev/null; then
    add_check "fstab" 1 10 10 "fstab has /srv/share entry using UUID or LABEL"
  else
    add_check "fstab" 0 0 10 "Missing/incorrect fstab entry for /srv/share"
  fi

  if [[ -d /srv/share/projects ]]; then
    local mode owner group
    mode="$(stat -c %a /srv/share/projects 2>/dev/null || echo "")"
    owner="$(stat -c %U /srv/share/projects 2>/dev/null || echo "")"
    group="$(stat -c %G /srv/share/projects 2>/dev/null || echo "")"
    if [[ "$owner" == "root" && "$group" == "ops" ]]; then
      add_check "dir_owner" 1 10 10 "projects owned by root:ops"
    else
      add_check "dir_owner" 0 0 10 "projects owner/group is ${owner}:${group} (expected root:ops)"
    fi
    # expect 2770 (setgid)
    if [[ "$mode" == "2770" ]]; then
      add_check "dir_mode" 1 10 10 "projects mode is 2770"
    else
      add_check "dir_mode" 0 0 10 "projects mode is $mode (expected 2770)"
    fi
  else
    add_check "projects_dir" 0 0 20 "/srv/share/projects missing"
  fi

  # ACL checks
  if have_cmd getfacl && [[ -d /srv/share/projects ]]; then
    local acl
    acl="$(getfacl -p /srv/share/projects 2>/dev/null || true)"
    if echo "$acl" | grep -qE '^user:examuser:rwx'; then
      add_check "acl_examuser" 1 10 10 "ACL for examuser rwx"
    else
      add_check "acl_examuser" 0 0 10 "Missing ACL user:examuser:rwx"
    fi
    if echo "$acl" | grep -qE '^user:auditor:r-x'; then
      add_check "acl_auditor" 1 10 10 "ACL for auditor r-x"
    else
      add_check "acl_auditor" 0 0 10 "Missing ACL user:auditor:r-x"
    fi
  else
    add_check "acl_tools" 0 0 10 "getfacl missing or projects dir missing"
  fi

  # script + timer
  if [[ -x /usr/local/bin/share-backup.sh ]]; then
    add_check "backup_script" 1 10 10 "share-backup.sh exists and is executable"
  else
    add_check "backup_script" 0 0 10 "Missing/executable bit not set for /usr/local/bin/share-backup.sh"
  fi

  if systemctl is-enabled share-backup.timer >/dev/null 2>&1; then
    add_check "timer_enabled" 1 5 5 "share-backup.timer enabled"
  else
    add_check "timer_enabled" 0 0 5 "share-backup.timer not enabled"
  fi
  if systemctl is-active share-backup.timer >/dev/null 2>&1; then
    add_check "timer_active" 1 5 5 "share-backup.timer active"
  else
    add_check "timer_active" 0 0 5 "share-backup.timer not active"
  fi

  # archive bonus
  if [[ -f /var/tmp/projects-backup.tar.gz ]]; then
    add_check "archive_present" 1 5 5 "Archive /var/tmp/projects-backup.tar.gz exists"
  else
    add_check "archive_present" 0 0 5 "Archive missing (bonus)"
  fi
}

# ---------- scenario: ssh-hardening-firewall ----------
grade_ssh_hardening_firewall() {
  # selinux enforcing
  if have_cmd getenforce && [[ "$(getenforce 2>/dev/null)" == "Enforcing" ]]; then
    add_check "selinux_enforcing" 1 10 10 "SELinux enforcing"
  else
    add_check "selinux_enforcing" 0 0 10 "SELinux not enforcing"
  fi

  # sshd enabled + active
  if systemctl is-enabled sshd >/dev/null 2>&1; then
    add_check "sshd_enabled" 1 10 10 "sshd enabled"
  else
    add_check "sshd_enabled" 0 0 10 "sshd not enabled"
  fi
  if systemctl is-active sshd >/dev/null 2>&1; then
    add_check "sshd_active" 1 10 10 "sshd active"
  else
    add_check "sshd_active" 0 0 10 "sshd not active"
  fi

  # sshd -T config checks (best)
  if have_cmd sshd; then
    local t
    t="$(sshd -T 2>/dev/null || true)"
    if echo "$t" | grep -qE '^passwordauthentication no$'; then
      add_check "passwordauth_no" 1 10 10 "PasswordAuthentication disabled"
    else
      add_check "passwordauth_no" 0 0 10 "PasswordAuthentication not disabled (check sshd_config)"
    fi
    if echo "$t" | grep -qE '^allowusers .*examuser(\s|$)'; then
      add_check "allowusers" 1 10 10 "AllowUsers includes examuser"
    else
      add_check "allowusers" 0 0 10 "AllowUsers does not include examuser"
    fi
  else
    add_check "sshd_T" 0 0 20 "sshd command not found"
  fi

  # authorized_keys + perms
  if id examuser >/dev/null 2>&1; then
    add_check "user_exists" 1 5 5 "examuser exists"
    if [[ -f /home/examuser/.ssh/authorized_keys ]]; then
      local dmode fmode downer fowner
      dmode="$(stat -c %a /home/examuser/.ssh 2>/dev/null || echo "")"
      fmode="$(stat -c %a /home/examuser/.ssh/authorized_keys 2>/dev/null || echo "")"
      downer="$(stat -c %U /home/examuser/.ssh 2>/dev/null || echo "")"
      fowner="$(stat -c %U /home/examuser/.ssh/authorized_keys 2>/dev/null || echo "")"
      if [[ "$downer" == "examuser" && "$fowner" == "examuser" ]]; then
        add_check "ssh_owner" 1 5 5 "authorized_keys owned by examuser"
      else
        add_check "ssh_owner" 0 0 5 "Ownership incorrect on .ssh/authorized_keys"
      fi
      # typical perms 700 and 600 (accept 750/640 sometimes)
      if [[ "$dmode" == "700" || "$dmode" == "750" ]]; then
        add_check "ssh_dir_mode" 1 5 5 ".ssh permissions OK ($dmode)"
      else
        add_check "ssh_dir_mode" 0 0 5 ".ssh permissions suspicious ($dmode)"
      fi
      if [[ "$fmode" == "600" || "$fmode" == "640" ]]; then
        add_check "ssh_file_mode" 1 5 5 "authorized_keys permissions OK ($fmode)"
      else
        add_check "ssh_file_mode" 0 0 5 "authorized_keys permissions suspicious ($fmode)"
      fi
    else
      add_check "authorized_keys" 0 0 15 "authorized_keys missing"
    fi
  else
    add_check "user_exists" 0 0 20 "examuser missing"
  fi

  # firewall ssh service permanent + runtime
  if have_cmd firewall-cmd; then
    local zone
    zone="$(firewall-cmd --get-default-zone 2>/dev/null || echo public)"
    if firewall-cmd --permanent --zone="$zone" --list-services 2>/dev/null | tr ' ' '\n' | grep -qx "ssh"; then
      add_check "fw_perm_ssh" 1 10 10 "ssh allowed permanently in firewalld"
    else
      add_check "fw_perm_ssh" 0 0 10 "ssh not allowed permanently in firewalld"
    fi
    if firewall-cmd --zone="$zone" --list-services 2>/dev/null | tr ' ' '\n' | grep -qx "ssh"; then
      add_check "fw_runtime_ssh" 1 5 5 "ssh allowed in runtime firewalld"
    else
      add_check "fw_runtime_ssh" 0 0 5 "ssh not allowed in runtime firewalld"
    fi
  else
    add_check "firewalld_cmd" 0 0 10 "firewall-cmd not found"
  fi
}

# ---------- scenario: nfs-autofs-client ----------
grade_nfs_autofs_client() {
  if rpm -q autofs >/dev/null 2>&1; then
    add_check "autofs_pkg" 1 10 10 "autofs installed"
  else
    add_check "autofs_pkg" 0 0 10 "autofs not installed"
  fi
  if rpm -q nfs-utils >/dev/null 2>&1; then
    add_check "nfs_utils" 1 5 5 "nfs-utils installed"
  else
    add_check "nfs_utils" 0 0 5 "nfs-utils not installed (may be required)"
  fi

  if systemctl is-enabled autofs >/dev/null 2>&1; then
    add_check "autofs_enabled" 1 10 10 "autofs enabled"
  else
    add_check "autofs_enabled" 0 0 10 "autofs not enabled"
  fi
  if systemctl is-active autofs >/dev/null 2>&1; then
    add_check "autofs_active" 1 10 10 "autofs active"
  else
    add_check "autofs_active" 0 0 10 "autofs not active"
  fi

  # config contains the NFS export
  if grep -Rqs "10.10.10.30:/exports/public" /etc/auto.* /etc/auto.master /etc/auto.master.d/* 2>/dev/null; then
    add_check "autofs_config" 1 20 20 "autofs config references 10.10.10.30:/exports/public"
  else
    add_check "autofs_config" 0 0 20 "autofs config missing reference to 10.10.10.30:/exports/public"
  fi

  # mount effective (best effort)
  if findmnt -n /mnt/public >/dev/null 2>&1; then
    add_check "mount_effective" 1 10 10 "/mnt/public is mounted"
  else
    add_check "mount_effective" 0 0 10 "/mnt/public not mounted (server may be unreachable)"
  fi
}

# ---------- scenario: packages-repo-troubleshoot ----------
grade_packages_repo_troubleshoot() {
  if [[ -f /etc/yum.repos.d/exam.repo ]]; then
    add_check "exam_repo_file" 1 10 10 "exam.repo exists"
    if grep -qsE '^\[exam\]' /etc/yum.repos.d/exam.repo && grep -qsE '^\s*enabled\s*=\s*1' /etc/yum.repos.d/exam.repo; then
      add_check "exam_repo_enabled" 1 10 10 "exam repo enabled=1"
    else
      add_check "exam_repo_enabled" 0 0 10 "exam repo missing [exam] or enabled=1"
    fi
    if grep -qsE '^\s*baseurl\s*=' /etc/yum.repos.d/exam.repo; then
      add_check "exam_repo_baseurl" 1 5 5 "exam repo has baseurl"
    else
      add_check "exam_repo_baseurl" 0 0 5 "exam repo missing baseurl"
    fi
  else
    add_check "exam_repo_file" 0 0 25 "Missing /etc/yum.repos.d/exam.repo"
  fi

  if rpm -q tree >/dev/null 2>&1; then
    add_check "tree_installed" 1 15 15 "tree installed"
  else
    add_check "tree_installed" 0 0 15 "tree not installed"
  fi
}

# ---------- scenario: flatpak-repo-app-hard ----------
grade_flatpak_repo_app_hard() {
  if have_cmd flatpak; then
    add_check "flatpak_cmd" 1 10 10 "flatpak command present"
  else
    add_check "flatpak_cmd" 0 0 10 "flatpak not installed"
    return
  fi

  local rem
  rem="$(flatpak remotes 2>/dev/null | awk 'NR>0{print $1}' | head -n 5 || true)"
  if [[ -n "$rem" ]]; then
    add_check "flatpak_remote" 1 10 10 "At least one remote configured"
  else
    add_check "flatpak_remote" 0 0 10 "No flatpak remote configured"
  fi

  if flatpak remotes 2>/dev/null | awk '{print $1}' | grep -qx "flathub"; then
    add_check "flatpak_flathub" 1 5 5 "Remote flathub present"
  else
    add_check "flatpak_flathub" 0 0 5 "Remote flathub not detected (OK if lab uses a different name)"
  fi

  local apps
  apps="$(flatpak list --app 2>/dev/null | wc -l | tr -d ' ' || echo 0)"
  if [[ "$apps" -ge 1 ]]; then
    add_check "flatpak_app" 1 15 15 "At least one Flatpak app installed"
  else
    add_check "flatpak_app" 0 0 15 "No Flatpak app installed"
  fi
}

# ---------- dispatch ----------
case "$SCENARIO_ID" in
  users-sudo-ssh) grade_users_sudo_ssh ;;
  storage-lvm-mount) grade_storage_lvm_mount ;;
  selinux-context-port) grade_selinux_context_port ;;
  networking-nmcli-hosts) grade_networking_nmcli_hosts ;;
  firewalld-service) grade_firewalld_service ;;
  systemd-timer) grade_systemd_timer ;;
  journald-persist) grade_journald_persist ;;
  time-sync-client) grade_time_sync_client ;;
  webstack-httpd-8081) grade_webstack_httpd_8081 ;;
  storage-lvm-acl-backup) grade_storage_lvm_acl_backup ;;
  ssh-hardening-firewall) grade_ssh_hardening_firewall ;;
  nfs-autofs-client) grade_nfs_autofs_client ;;
  packages-repo-troubleshoot) grade_packages_repo_troubleshoot ;;
  flatpak-repo-app-hard) grade_flatpak_repo_app_hard ;;
  *)
    echo "Unknown scenario_id: $SCENARIO_ID" >&2
    echo "Known: users-sudo-ssh, storage-lvm-mount, selinux-context-port, networking-nmcli-hosts, firewalld-service, systemd-timer, journald-persist, time-sync-client, webstack-httpd-8081, storage-lvm-acl-backup, ssh-hardening-firewall, nfs-autofs-client, packages-repo-troubleshoot, flatpak-repo-app-hard" >&2
    exit 2
    ;;
esac

PCT=0
if (( MAX > 0 )); then
  PCT=$(( SCORE * 100 / MAX ))
fi

JSON=$(printf '{"scenario":"%s","score":%d,"max":%d,"percent":%d,"checks":[%s]}' \
  "$(json_escape "$SCENARIO_ID")" "$SCORE" "$MAX" "$PCT" "$CHECKS_JSON")

echo "$JSON"
echo
echo "---- Summary ----"
echo "Scenario : $SCENARIO_ID"
echo "Score    : $SCORE / $MAX ($PCT%)"
echo "Checks   : $CHECK_COUNT"
