#!/usr/bin/env bash
set -Eeuo pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
FIXTURE_DIR="$SCRIPT_DIR/fixtures/lot3"
MARKER='.soria-hardening-linux'
EXPECTED_DECISION='LINUX_HARDENING_WITH_ACTIONS'

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

usage() {
  cat <<'USAGE'
Usage: module3-linux-hardening.sh <setup|status|run|validate|reset> <root>
The root must match /tmp/soria-hardening-* or $HOME/soria-hardening-*.
USAGE
}

safe_root() {
  local root=${1:-}
  [[ -n "$root" ]] || fail 'missing lab root'
  [[ "$root" = /* ]] || fail 'lab root must be absolute'
  [[ "$root" != '/' && "$root" != '/tmp' && "$root" != "$HOME" ]] || fail 'unsafe lab root'
  case "$root" in
    /tmp/soria-hardening-*|"$HOME"/soria-hardening-*) ;;
    *) fail "lab root outside allowed patterns: $root" ;;
  esac
}

require_marker() {
  local root=$1
  [[ -f "$root/$MARKER" ]] || fail "missing marker: $root/$MARKER"
}

csv_unique() {
  local file=$1 col=$2
  awk -F, -v c="$col" 'NR==1{next} {if($c=="" || seen[$c]++){exit 1}}' "$file"
}

csv_nonempty() {
  local file=$1; shift
  local cols="$*"
  awk -F, -v cols="$cols" '
    BEGIN{n=split(cols,a," ")}
    NR==1{next}
    {for(i=1;i<=n;i++) if($(a[i])=="") exit 1}
  ' "$file"
}

write_manifest() {
  local base=$1 out=$2
  {
    echo 'relative_path,sha256'
    find "$base" -type f -print | sort | while IFS= read -r file; do
      rel=${file#"$base"/}
      hash=$(sha256sum "$file" | awk '{print $1}')
      printf '%s,%s\n' "$rel" "$hash"
    done
  } > "$out"
}

validate_cross_refs() {
  local root=$1
  local hosts="$root/work/target/inventory/hosts.csv"
  local accounts="$root/work/target/accounts/accounts.csv"

  awk -F, -v hosts="$hosts" '
    BEGIN{while((getline l < hosts)>0){if(++n==1)continue; split(l,a,","); ids[a[1]]=1}}
    NR==1{next} !ids[$2]{print "unknown host reference: "$2 > "/dev/stderr"; bad=1}
    END{exit bad}
  ' "$accounts"

  for file in \
    "$root/work/target/accounts/pam-policy.csv" \
    "$root/work/target/accounts/sudo-rules.csv" \
    "$root/work/target/access/ssh-policy.csv" \
    "$root/work/target/filesystem/files.csv" \
    "$root/work/target/filesystem/mounts.csv" \
    "$root/work/target/software/packages.csv" \
    "$root/work/target/software/services.csv" \
    "$root/work/target/execution/capabilities.csv" \
    "$root/work/target/execution/systemd-units.csv"
  do
    awk -F, -v hosts="$hosts" '
      BEGIN{while((getline l < hosts)>0){if(++n==1)continue; split(l,a,","); ids[a[1]]=1}}
      NR==1{next} !ids[$2]{print "unknown host reference: "$2 > "/dev/stderr"; bad=1}
      END{exit bad}
    ' "$file"
  done

  awk -F, -v accounts="$accounts" '
    BEGIN{while((getline l < accounts)>0){if(++n==1)continue; split(l,a,","); ids[a[1]]=1}}
    NR==1{next} !ids[$3]{print "unknown sudo subject: "$3 > "/dev/stderr"; bad=1}
    END{exit bad}
  ' "$root/work/target/accounts/sudo-rules.csv"
}

validate_controls_file() {
  local roles=$1 controls=$2
  awk -F, -v roles="$roles" '
    BEGIN{while((getline l < roles)>0){if(++n==1)continue; split(l,a,","); known[a[1]]=1}}
    NR==1{next}
    !known[$2] || $7=="" || $8=="" || $9=="" || $10=="" {
      print "baseline control missing role owner justification test or rollback" > "/dev/stderr"; bad=1
    }
    END{exit bad}
  ' "$controls"
}

validate_admin_access_files() {
  local accounts=$1 ssh=$2 services=$3
  local admin_ok root_ok password_ok allow_ok service_ok
  admin_ok=$(awk -F, 'NR>1 && $1=="admin-ops" && $2=="lin-bastion-01" && $3=="privileged-admin" && $4!="" && $9=="no" && $7=="/bin/bash"{print "yes"}' "$accounts")
  root_ok=$(awk -F, 'NR>1 && $2=="lin-bastion-01" && $3=="PermitRootLogin" && $5=="no"{print "yes"}' "$ssh")
  password_ok=$(awk -F, 'NR>1 && $2=="lin-bastion-01" && $3=="PasswordAuthentication" && $5=="no"{print "yes"}' "$ssh")
  allow_ok=$(awk -F, 'NR>1 && $2=="lin-bastion-01" && $3=="AllowUsers" && $5 ~ /(^|\|)admin-ops($|\|)/{print "yes"}' "$ssh")
  service_ok=$(awk -F, 'NR>1 && $2=="lin-bastion-01" && $3=="sshd" && $5=="enabled" && $6=="yes"{print "yes"}' "$services")
  [[ "$admin_ok" == yes && "$root_ok" == yes && "$password_ok" == yes && "$allow_ok" == yes && "$service_ok" == yes ]] || {
    echo 'administrative access path lost' >&2
    return 1
  }
}

validate_sudo_file() {
  local file=$1
  awk -F, '
    NR==1{next}
    $5=="ALL" || $5 ~ /[*]/ || $7=="" || $8=="" || $9=="" {
      print "sudo rule remains overbroad" > "/dev/stderr"; bad=1
    }
    END{exit bad}
  ' "$file"
}

validate_files_file() {
  local file=$1
  awk -F, '
    NR==1{next}
    {
      last=substr($7,length($7),1)
      if($4=="" || ($8=="yes" && last ~ /[2367]/)) {
        print "sensitive file is world-writable or ownerless" > "/dev/stderr"; bad=1
      }
    }
    END{exit bad}
  ' "$file"
}

validate_mounts_file() {
  local file=$1
  awk -F, '
    NR==1{next}
    $5 !~ /(^|\|)nodev($|\|)/ || $5 !~ /(^|\|)nosuid($|\|)/ {
      print "mount policy missing required option" > "/dev/stderr"; bad=1
    }
    $6=="yes" && $5 ~ /(^|\|)noexec($|\|)/ {
      print "mount policy conflicts with functional requirement" > "/dev/stderr"; bad=1
    }
    $3=="/tmp" && $6=="no" && $5 !~ /(^|\|)noexec($|\|)/ {
      print "mount policy missing required option" > "/dev/stderr"; bad=1
    }
    END{exit bad}
  ' "$file"
}

validate_software_files() {
  local packages=$1 services=$2
  awk -F, '
    NR==1{next}
    $6=="yes" && $5!="installed" {print "required package or service removed" > "/dev/stderr"; bad=1}
    $6=="no" && ($3=="telnet" || $3=="ftp") && $5!="absent" {print "unused package remains" > "/dev/stderr"; bad=1}
    END{exit bad}
  ' "$packages" || return 1
  awk -F, '
    NR==1{next}
    $6=="yes" && $5!="enabled" {print "required package or service removed" > "/dev/stderr"; bad=1}
    $6=="no" && ($3=="avahi-daemon" || $3=="rpcbind") && $5!="disabled" {print "unused service remains" > "/dev/stderr"; bad=1}
    END{exit bad}
  ' "$services"
}

validate_execution_files() {
  local caps=$1 units=$2
  awk -F, '
    NR==1{next}
    $7=="no" && $6!="absent" {print "execution privilege remains excessive" > "/dev/stderr"; bad=1}
    END{exit bad}
  ' "$caps" || return 1
  awk -F, '
    NR==1{next}
    $10=="yes" && ($5=="root" || $7!="yes" || $9=="no") {
      print "execution privilege remains excessive" > "/dev/stderr"; bad=1
    }
    END{exit bad}
  ' "$units"
}

validate_change_files() {
  local plan=$1 prechecks=$2 rollback=$3
  awk -F, '
    NR==1{next}
    $6=="" || $7=="" || $8=="" || $9=="" {print "change is missing precheck stop test or rollback" > "/dev/stderr"; bad=1}
    END{exit bad}
  ' "$plan" || return 1
  awk -F, '
    NR==1{next}
    $4=="" || $5!="PASS" || $6=="" || $7=="" || $8!="yes" {
      print "change is missing precheck stop test or rollback" > "/dev/stderr"; bad=1
    }
    END{exit bad}
  ' "$prechecks" || return 1
  awk -F, '
    NR==1{next}
    $3=="" || $4=="" || $5=="" || $6=="" || $7=="" {
      print "change is missing precheck stop test or rollback" > "/dev/stderr"; bad=1
    }
    END{exit bad}
  ' "$rollback"
}

validate_functional_state() {
  local root=$1 req="$root/source/functional/workload-requirements.csv"
  local packages="$root/work/target/software/packages.csv"
  local services="$root/work/target/software/services.csv"
  local accounts="$root/work/target/accounts/accounts.csv"
  local mounts="$root/work/target/filesystem/mounts.csv"
  awk -F, -v packages="$packages" -v services="$services" -v accounts="$accounts" -v mounts="$mounts" '
    BEGIN{
      while((getline l < packages)>0){if(++np==1)continue; split(l,a,","); pkg[a[2] SUBSEP a[3]]=a[5]}
      while((getline l < services)>0){if(++ns==1)continue; split(l,a,","); svc[a[2] SUBSEP a[3]]=a[5]}
      while((getline l < accounts)>0){if(++na==1)continue; split(l,a,","); acct[a[2] SUBSEP a[1]]=a[9]}
      while((getline l < mounts)>0){if(++nm==1)continue; split(l,a,","); mnt[a[2] SUBSEP a[1]]=a[5]}
    }
    NR==1{next}
    pkg[$2 SUBSEP $4]!="installed" || svc[$2 SUBSEP $5]!="enabled" || acct[$2 SUBSEP $6]!="no" || mnt[$2 SUBSEP $7]=="" {
      print "functional requirement failed: "$1 > "/dev/stderr"; bad=1
    }
    END{exit bad}
  ' "$req"
}

expect_reject() {
  local label=$1 reason=$2; shift 2
  local output
  if output=$("$@" 2>&1); then
    fail "negative variant accepted: $label"
  fi
  grep -Fq "$reason" <<<"$output" || fail "negative variant rejected for unexpected reason: $label :: $output"
  printf '%s,%s,%s\n' "$label" BLOCKED "$reason" >> "$NEGATIVE_REPORT"
}

setup_lab() {
  local root=$1
  safe_root "$root"
  if [[ -e "$root" ]]; then
    require_marker "$root"
    chmod -R u+w "$root"
    rm -rf -- "$root"
  fi
  mkdir -p "$root"/{source,state,work,output,negative,rollback}
  printf 'SORIA hardening Lot 3 disposable lab\n' > "$root/$MARKER"
  [[ -d "$FIXTURE_DIR" ]] || fail "missing fixture directory: $FIXTURE_DIR"
  cp -a "$FIXTURE_DIR/." "$root/source/"
  find "$root/source" -type f -print0 | sort -z | xargs -0 sha256sum > "$root/state/source.sha256"
  chmod -R a-w "$root/source"
  info "Lot 3 setup complete: $root"
}

status_lab() {
  local root=$1
  safe_root "$root"; require_marker "$root"
  printf 'root=%s\n' "$root"
  printf 'source_files=%s\n' "$(find "$root/source" -type f | wc -l | tr -d ' ')"
  printf 'outputs=%s\n' "$(find "$root/output" -type f 2>/dev/null | wc -l | tr -d ' ')"
  [[ -f "$root/output/final-decision.txt" ]] && cat "$root/output/final-decision.txt" || true
}

transform_target() {
  local root=$1
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$5=$6; print}' "$root/work/before/baseline/controls.csv" > "$root/work/target/baseline/controls.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$6=$7; $8=$9; print}' "$root/work/before/accounts/accounts.csv" > "$root/work/target/accounts/accounts.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$4=$5; print}' "$root/work/before/accounts/pam-policy.csv" > "$root/work/target/accounts/pam-policy.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$4=$5; print}' "$root/work/before/accounts/sudo-rules.csv" > "$root/work/target/accounts/sudo-rules.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$4=$5; print}' "$root/work/before/access/ssh-policy.csv" > "$root/work/target/access/ssh-policy.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$6=$7; print}' "$root/work/before/filesystem/files.csv" > "$root/work/target/filesystem/files.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$4=$5; print}' "$root/work/before/filesystem/mounts.csv" > "$root/work/target/filesystem/mounts.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$4=$5; print}' "$root/work/before/software/packages.csv" > "$root/work/target/software/packages.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$4=$5; print}' "$root/work/before/software/services.csv" > "$root/work/target/software/services.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$5=$6; print}' "$root/work/before/execution/capabilities.csv" > "$root/work/target/execution/capabilities.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$4=$5; $6=$7; $8=$9; print}' "$root/work/before/execution/systemd-units.csv" > "$root/work/target/execution/systemd-units.csv"
}

run_lab() {
  local root=$1
  safe_root "$root"; require_marker "$root"
  rm -rf "$root/work" "$root/output" "$root/negative" "$root/rollback"
  mkdir -p "$root/work"/{before,target,generated} "$root/output" "$root/negative" "$root/rollback"
  cp -a "$root/source/." "$root/work/before/"
  cp -a "$root/source/." "$root/work/target/"
  chmod -R u+w "$root/work"
  transform_target "$root"

  cp "$root/source/baseline/controls.csv" "$root/output/linux-role-baseline.csv"
  {
    echo 'control_id,role,category,setting,before,target,assessment,owner,test_id,rollback_id,proof_limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {assessment=($5==$6?"ALIGNED":"CHANGE"); print $1,$2,$3,$4,$5,$6,assessment,$8,$9,$10,$11}' "$root/source/baseline/controls.csv"
  } > "$root/output/control-assessment.csv"
  {
    echo 'artifact_id,artifact,before,target,result,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} $5!=$6{print $1,"baseline-control",$5,$6,"APPLIED-TO-COPY",$11}' "$root/source/baseline/controls.csv"
    awk -F, 'BEGIN{OFS=","} NR==1{next} $4!=$5{print $1,"sudo-scope",$4,$5,"APPLIED-TO-COPY","no real sudo changed"}' "$root/source/accounts/sudo-rules.csv"
    awk -F, 'BEGIN{OFS=","} NR==1{next} $4!=$5{print $1,"ssh-policy",$4,$5,"APPLIED-TO-COPY",$9}' "$root/source/access/ssh-policy.csv"
    awk -F, 'BEGIN{OFS=","} NR==1{next} $6!=$7{print $1,"file-mode",$6,$7,"APPLIED-TO-COPY","no real mode changed"}' "$root/source/filesystem/files.csv"
    awk -F, 'BEGIN{OFS=","} NR==1{next} $4!=$5{print $1,"mount-options",$4,$5,"APPLIED-TO-COPY","no real mount changed"}' "$root/source/filesystem/mounts.csv"
    awk -F, 'BEGIN{OFS=","} NR==1{next} $4!=$5{print $1,"package",$4,$5,"APPLIED-TO-COPY","no real package changed"}' "$root/source/software/packages.csv"
    awk -F, 'BEGIN{OFS=","} NR==1{next} $4!=$5{print $1,"service",$4,$5,"APPLIED-TO-COPY","no real service changed"}' "$root/source/software/services.csv"
  } > "$root/output/effective-delta.csv"

  {
    echo 'account_id,host_id,type,owner,before_shell,target_shell,before_locked,target_locked,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($7!=$8||$9!=$10?"CHANGE":"KEEP"); print $1,$2,$3,$4,$6,$7,$8,$9,decision,"no real account changed"}' "$root/source/accounts/accounts.csv"
  } > "$root/output/account-hardening-review.csv"
  {
    echo 'policy_id,host_id,control,before,target,owner,test_id,exception_id,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {print $1,$2,$3,$4,$5,$6,$7,$8,"WITH_ACTIONS",$9}' "$root/source/accounts/pam-policy.csv"
  } > "$root/output/pam-review.csv"
  {
    echo 'rule_id,host_id,subject,before_scope,target_scope,nopasswd,justification,owner,revocation,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($4!=$5?"RESTRICT":"KEEP"); print $1,$2,$3,$4,$5,$6,$7,$8,$9,decision,"no real sudo evaluated"}' "$root/source/accounts/sudo-rules.csv"
  } > "$root/output/sudo-review.csv"
  {
    echo 'policy_id,host_id,directive,before,target,owner,test_id,rollback_id,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($4!=$5?"CHANGE":"KEEP"); print $1,$2,$3,$4,$5,$6,$7,$8,decision,$9}' "$root/source/access/ssh-policy.csv"
  } > "$root/output/ssh-review.csv"
  {
    echo 'file_id,host_id,path,owner,group,before_mode,target_mode,sensitive,test_id,rollback_id,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($6!=$7?"CHANGE":"KEEP"); print $1,$2,$3,$4,$5,$6,$7,$8,$10,$11,decision,"metadata fixture only"}' "$root/source/filesystem/files.csv"
  } > "$root/output/file-permissions-review.csv"
  {
    echo 'mount_id,host_id,mount_point,before_options,target_options,required_exec,owner,test_id,exception_id,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($4!=$5?"CHANGE":"KEEP"); print $1,$2,$3,$4,$5,$6,$7,$8,$9,decision,"no real mount changed"}' "$root/source/filesystem/mounts.csv"
  } > "$root/output/mount-hardening-review.csv"
  {
    echo 'package_id,host_id,package,before,target,required,justification,owner,functional_test,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($4!=$5?"REMOVE":"KEEP"); print $1,$2,$3,$4,$5,$6,$7,$8,$9,decision,"no package manager used"}' "$root/source/software/packages.csv"
  } > "$root/output/package-reduction-plan.csv"
  {
    echo 'service_id,host_id,service,before,target,required,listen_scope,owner,functional_test,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($4!=$5?"DISABLE":"KEEP"); print $1,$2,$3,$4,$5,$6,$7,$8,$9,decision,"no service manager used"}' "$root/source/software/services.csv"
  } > "$root/output/service-reduction-plan.csv"
  {
    echo 'item_id,host_id,item,before,target,required,owner,decision,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($5!=$6?"REDUCE":"KEEP"); print $1,$2,$3" "$4,$5,$6,$7,$8,decision,"no capability changed"}' "$root/source/execution/capabilities.csv"
    awk -F, 'BEGIN{OFS=","} NR==1{next} {decision=($4!=$5||$6!=$7||$8!=$9?"REDUCE":"KEEP"); print $1,$2,$3,$4"|"$6"|"$8,$5"|"$7"|"$9,$10,$11,decision,"no unit changed"}' "$root/source/execution/systemd-units.csv"
  } > "$root/output/execution-privilege-review.csv"
  cp "$root/source/change/change-plan.csv" "$root/output/change-readiness.csv"
  cp "$root/source/change/prechecks.csv" "$root/output/precheck-report.csv"

  {
    echo 'test_id,control,expected,result,evidence,limit'
    echo 'TEST-SSH-ROOT,root-login,denied,PASS,fixture-target,no real login attempted'
    echo 'TEST-SSH-PASSWORD,password-login,denied,PASS,fixture-target,no real login attempted'
    echo 'TEST-SSH-ALLOW,named-admin,allowed,PASS,fixture-target,no real login attempted'
    echo 'TEST-SUDO,bounded-command,allowed,PASS,fixture-target,no real sudo executed'
  } > "$root/output/access-test-report.csv"
  {
    echo 'test_id,host_id,function,expected,result,evidence,limit'
    awk -F, 'BEGIN{OFS=","} NR==1{next} {print $1,$2,$3,$8,"PASS","fixture-cross-check",$10}' "$root/source/functional/workload-requirements.csv"
  } > "$root/output/functional-test-report.csv"
  {
    echo 'exception_id,scope,reason,owner,expiration,action,decision'
    echo 'EXC-PAM-REAL,PAM-enforcement,fixture-only,team-platform,2026-09-30,test-on-authorized-host,WITH_ACTIONS'
    echo 'EXC-MOUNT-EXEC,required-execution,documented-workload,team-platform,2026-09-30,retest-helper-and-remove-if-possible,WITH_ACTIONS'
    echo 'EXC-LINUX-REAL,all-controls,no-real-host-proof,team-platform,2026-09-30,deploy-by-ring-and-collect-evidence,WITH_ACTIONS'
  } > "$root/output/exceptions.csv"

  cat > "$root/work/generated/sshd_config.fragment" <<'EOF_FRAG'
# SYNTHETIC — DO NOT APPLY
PermitRootLogin no
PasswordAuthentication no
AllowUsers admin-ops
MaxAuthTries 3
EOF_FRAG
  cat > "$root/work/generated/sudoers.fragment" <<'EOF_FRAG'
# SYNTHETIC — DO NOT APPLY
admin-ops HOSTS = systemctl-restart-soria-web
EOF_FRAG
  cat > "$root/work/generated/fstab.fragment" <<'EOF_FRAG'
# SYNTHETIC — DO NOT APPLY
/tmp options=nodev|nosuid|noexec
/home options=nodev|nosuid
/var/tmp/backup-work options=nodev|nosuid
EOF_FRAG
  cat > "$root/work/generated/systemd-hardening.fragment" <<'EOF_FRAG'
# SYNTHETIC — DO NOT APPLY
User=soria-web
NoNewPrivileges=yes
ProtectSystem=strict
EOF_FRAG

  cp -a "$root/work/before/." "$root/rollback/restored/"
  write_manifest "$root/work/before" "$root/output/rollback-manifest.csv"
  write_manifest "$root/rollback/restored" "$root/rollback/restored-manifest.csv"
  if cmp -s "$root/output/rollback-manifest.csv" "$root/rollback/restored-manifest.csv"; then
    cat > "$root/output/rollback-report.txt" <<'EOF_ROLLBACK'
result=PASS
method=restore work-before copy
verification=manifest-match
functional_state=represented requirements restored
limit=no real Linux host restored
EOF_ROLLBACK
  else
    fail 'rollback manifest mismatch'
  fi

  cat > "$root/output/proof-boundaries.md" <<'EOF_PROOF'
# Frontières de preuve

Le lab prouve la cohérence de fixtures synthétiques, de deltas, de tests représentés et d'un rollback par copie.

Il ne prouve pas l'application réelle de PAM, sudo, SSH, permissions, montages, paquets, services, capacités Linux ou unités systemd. Il ne tente aucune connexion et ne modifie aucun hôte.
EOF_PROOF
  printf '%s\n' "$EXPECTED_DECISION" > "$root/output/final-decision.txt"
  info "Lot 3 run complete: $root"
}

validate_lab() {
  local root=$1
  safe_root "$root"; require_marker "$root"
  [[ -d "$root/work/target" && -d "$root/output" ]] || fail 'run must complete before validate'

  sha256sum -c "$root/state/source.sha256" >/dev/null

  local file
  for file in \
    linux-role-baseline.csv control-assessment.csv effective-delta.csv \
    account-hardening-review.csv pam-review.csv sudo-review.csv ssh-review.csv \
    file-permissions-review.csv mount-hardening-review.csv package-reduction-plan.csv \
    service-reduction-plan.csv execution-privilege-review.csv change-readiness.csv \
    precheck-report.csv access-test-report.csv functional-test-report.csv exceptions.csv \
    rollback-manifest.csv rollback-report.txt final-decision.txt proof-boundaries.md
  do
    [[ -s "$root/output/$file" ]] || fail "missing output: $file"
  done

  [[ "$(cat "$root/output/final-decision.txt")" == "$EXPECTED_DECISION" ]] || fail 'unexpected reference decision'
  [[ "$(find "$root/source" -type f | wc -l | tr -d ' ')" -eq 17 ]] || fail 'unexpected fixture count'

  csv_unique "$root/source/inventory/hosts.csv" 1 || fail 'duplicate host id'
  csv_unique "$root/source/baseline/controls.csv" 1 || fail 'duplicate control id'
  csv_unique "$root/source/accounts/accounts.csv" 1 || fail 'duplicate account id'
  csv_unique "$root/source/accounts/pam-policy.csv" 1 || fail 'duplicate PAM policy id'
  csv_unique "$root/source/accounts/sudo-rules.csv" 1 || fail 'duplicate sudo rule id'
  csv_unique "$root/source/access/ssh-policy.csv" 1 || fail 'duplicate SSH policy id'
  csv_unique "$root/source/filesystem/files.csv" 1 || fail 'duplicate file id'
  csv_unique "$root/source/filesystem/mounts.csv" 1 || fail 'duplicate mount id'
  csv_unique "$root/source/software/packages.csv" 1 || fail 'duplicate package id'
  csv_unique "$root/source/software/services.csv" 1 || fail 'duplicate service id'
  csv_unique "$root/source/execution/capabilities.csv" 1 || fail 'duplicate capability id'
  csv_unique "$root/source/execution/systemd-units.csv" 1 || fail 'duplicate unit id'

  for role in web database bastion backup; do
    grep -Eq "^[^,]+,$role," "$root/source/inventory/hosts.csv" || fail "missing role: $role"
  done
  csv_nonempty "$root/source/inventory/hosts.csv" 1 2 3 4 5 6 7 || fail 'host metadata incomplete'

  validate_cross_refs "$root"
  validate_controls_file "$root/source/inventory/role-requirements.csv" "$root/work/target/baseline/controls.csv"
  validate_admin_access_files "$root/work/target/accounts/accounts.csv" "$root/work/target/access/ssh-policy.csv" "$root/work/target/software/services.csv"
  validate_sudo_file "$root/work/target/accounts/sudo-rules.csv"
  validate_files_file "$root/work/target/filesystem/files.csv"
  validate_mounts_file "$root/work/target/filesystem/mounts.csv"
  validate_software_files "$root/work/target/software/packages.csv" "$root/work/target/software/services.csv"
  validate_execution_files "$root/work/target/execution/capabilities.csv" "$root/work/target/execution/systemd-units.csv"
  validate_change_files "$root/source/change/change-plan.csv" "$root/source/change/prechecks.csv" "$root/source/change/rollback.csv"
  validate_functional_state "$root"

  grep -Fq 'SYNTHETIC — DO NOT APPLY' "$root/work/generated/sshd_config.fragment"
  grep -Fq 'SYNTHETIC — DO NOT APPLY' "$root/work/generated/sudoers.fragment"
  grep -Fq 'SYNTHETIC — DO NOT APPLY' "$root/work/generated/fstab.fragment"
  grep -Fq 'SYNTHETIC — DO NOT APPLY' "$root/work/generated/systemd-hardening.fragment"
  grep -Fq 'result=PASS' "$root/output/rollback-report.txt"
  cmp -s "$root/output/rollback-manifest.csv" "$root/rollback/restored-manifest.csv" || fail 'rollback verification failed'

  NEGATIVE_REPORT="$root/output/negative-test-report.csv"
  export NEGATIVE_REPORT
  echo 'variant,result,reason' > "$NEGATIVE_REPORT"

  mkdir -p "$root/negative/baseline" && cp "$root/work/target/baseline/controls.csv" "$root/negative/baseline/controls.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $1=="CTL-WEB-001"{$8=""} {print}' "$root/negative/baseline/controls.csv" > "$root/negative/baseline/tmp" && mv "$root/negative/baseline/tmp" "$root/negative/baseline/controls.csv"
  expect_reject baseline-orphan 'baseline control missing role owner justification test or rollback' validate_controls_file "$root/source/inventory/role-requirements.csv" "$root/negative/baseline/controls.csv"

  mkdir -p "$root/negative/admin" && cp "$root/work/target/accounts/accounts.csv" "$root/negative/admin/accounts.csv" && cp "$root/work/target/access/ssh-policy.csv" "$root/negative/admin/ssh.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $1=="admin-ops"{$9="yes"} {print}' "$root/negative/admin/accounts.csv" > "$root/negative/admin/tmp" && mv "$root/negative/admin/tmp" "$root/negative/admin/accounts.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $3=="AllowUsers"{$5="none"} {print}' "$root/negative/admin/ssh.csv" > "$root/negative/admin/tmp" && mv "$root/negative/admin/tmp" "$root/negative/admin/ssh.csv"
  expect_reject admin-path-loss 'administrative access path lost' validate_admin_access_files "$root/negative/admin/accounts.csv" "$root/negative/admin/ssh.csv" "$root/work/target/software/services.csv"

  mkdir -p "$root/negative/sudo" && cp "$root/work/target/accounts/sudo-rules.csv" "$root/negative/sudo/rules.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $1=="SUDO-001"{$5="ALL"} {print}' "$root/negative/sudo/rules.csv" > "$root/negative/sudo/tmp" && mv "$root/negative/sudo/tmp" "$root/negative/sudo/rules.csv"
  expect_reject sudo-overbroad 'sudo rule remains overbroad' validate_sudo_file "$root/negative/sudo/rules.csv"

  mkdir -p "$root/negative/files" && cp "$root/work/target/filesystem/files.csv" "$root/negative/files/files.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $1=="FILE-002"{$7="0666"} {print}' "$root/negative/files/files.csv" > "$root/negative/files/tmp" && mv "$root/negative/files/tmp" "$root/negative/files/files.csv"
  expect_reject unsafe-file 'sensitive file is world-writable or ownerless' validate_files_file "$root/negative/files/files.csv"

  mkdir -p "$root/negative/mounts" && cp "$root/work/target/filesystem/mounts.csv" "$root/negative/mounts/mounts.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $1=="MOUNT-003"{$5="nodev|nosuid|noexec"} {print}' "$root/negative/mounts/mounts.csv" > "$root/negative/mounts/tmp" && mv "$root/negative/mounts/tmp" "$root/negative/mounts/mounts.csv"
  expect_reject mount-function-conflict 'mount policy conflicts with functional requirement' validate_mounts_file "$root/negative/mounts/mounts.csv"

  mkdir -p "$root/negative/software" && cp "$root/work/target/software/packages.csv" "$root/negative/software/packages.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $1=="PKG-001"{$5="absent"} {print}' "$root/negative/software/packages.csv" > "$root/negative/software/tmp" && mv "$root/negative/software/tmp" "$root/negative/software/packages.csv"
  expect_reject required-package-removed 'required package or service removed' validate_software_files "$root/negative/software/packages.csv" "$root/work/target/software/services.csv"

  mkdir -p "$root/negative/execution" && cp "$root/work/target/execution/capabilities.csv" "$root/negative/execution/caps.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} $1=="CAP-001"{$6="present"} {print}' "$root/negative/execution/caps.csv" > "$root/negative/execution/tmp" && mv "$root/negative/execution/tmp" "$root/negative/execution/caps.csv"
  expect_reject excessive-execution 'execution privilege remains excessive' validate_execution_files "$root/negative/execution/caps.csv" "$root/work/target/execution/systemd-units.csv"

  mkdir -p "$root/negative/change" && cp "$root/source/change/change-plan.csv" "$root/negative/change/plan.csv"
  awk -F, 'BEGIN{OFS=","} NR==1{print;next} {$9=""; print}' "$root/negative/change/plan.csv" > "$root/negative/change/tmp" && mv "$root/negative/change/tmp" "$root/negative/change/plan.csv"
  expect_reject irreversible-change 'change is missing precheck stop test or rollback' validate_change_files "$root/negative/change/plan.csv" "$root/source/change/prechecks.csv" "$root/source/change/rollback.csv"

  [[ "$(awk 'END{print NR-1}' "$NEGATIVE_REPORT")" -eq 8 ]] || fail 'unexpected negative test count'
  {
    echo 'result=PASS'
    echo 'decision=LINUX_HARDENING_WITH_ACTIONS'
    echo 'fixture_count=17'
    echo 'roles=4'
    echo 'negative_variants=8'
    echo 'rollback=PASS'
    echo 'source_integrity=PASS'
    echo 'proof_scope=synthetic-fixtures-only'
  } > "$root/output/validation-summary.txt"

  info "Lot 3 validation passed: $root"
}

reset_lab() {
  local root=$1
  safe_root "$root"; require_marker "$root"
  chmod -R u+w "$root"
  rm -rf -- "$root"
  [[ ! -e "$root" ]] || fail 'reset cleanup failed'
  info "Lot 3 reset complete: $root"
}

main() {
  [[ $# -eq 2 ]] || { usage >&2; exit 2; }
  local action=$1 root=$2
  case "$action" in
    setup) setup_lab "$root" ;;
    status) status_lab "$root" ;;
    run) run_lab "$root" ;;
    validate) validate_lab "$root" ;;
    reset) reset_lab "$root" ;;
    *) usage >&2; exit 2 ;;
  esac
}

main "$@"
