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

ZIP_URL="${ZIP_URL:-https://8414.bwater.io/downloads/labs/packages/seas8414-blackboard-week-01-2026.05.0_4e76a52f_aws8414.zip}"
OUTER_ZIP="${OUTER_ZIP:-seas8414-blackboard-week-01-2026.05.0_4e76a52f_aws8414.zip}"
OUTER_DIR="${OUTER_DIR:-seas8414-blackboard-week-01-2026.05.0_4e76a52f_aws8414}"
RUNTIME_ZIP="${RUNTIME_ZIP:-runtime/seas8414-student-lab-2026.05.0+4e76a52f.aws8414.zip}"
RUNTIME_DIR="${RUNTIME_DIR:-seas8414-student-lab-2026.05.0+4e76a52f.aws8414}"
LAB_DIR="$OUTER_DIR/$RUNTIME_DIR/student-lab"
RESULTS_REL="lab-results/week-01"
EVIDENCE_REL="$RESULTS_REL/evidence"
SUBNET="${SUBNET:-172.30.0.0/24}"
API_BASE="${API_BASE:-http://localhost:8100}"
EMAIL="${EMAIL:-student@example.com}"
PASSWORD="${PASSWORD:-SecurePass!2026}"
FULL_SCAN_INTERVAL="${FULL_SCAN_INTERVAL:-15}"
FULL_SCAN_POLLS="${FULL_SCAN_POLLS:-80}"
DISCOVERY_SCAN_POLLS="${DISCOVERY_SCAN_POLLS:-40}"
KEEP_LAB="${KEEP_LAB:-0}"
DRY_RUN="${DRY_RUN:-0}"

ROOT_DIR="$(pwd)"

case "$ROOT_DIR" in
  /tmp/*|/private/tmp/*)
    if [ "$(uname -s)" = "Darwin" ]; then
      cat >&2 <<'EOF'
Do not run this Docker lab from /tmp on macOS.

Docker Desktop and Colima can reject bind mounts from temporary directories,
which makes valid files such as mosquitto.conf fail during container startup.
Create a normal working directory, then rerun:

  mkdir -p "$HOME/seas8414-week01"
  cd "$HOME/seas8414-week01"
  curl -fsSL https://8414.bwater.io/downloads/labs/scripts/run-week01-lab.sh -o run-week01-lab.sh
  chmod +x run-week01-lab.sh
  ./run-week01-lab.sh
EOF
      exit 1
    fi
    ;;
esac

if [ -f "$RUNTIME_ZIP" ] && [ -d "lab" ]; then
  OUTER_DIR="."
  LAB_DIR="$RUNTIME_DIR/student-lab"
fi

need() {
  command -v "$1" >/dev/null 2>&1 || {
    echo "Missing required command: $1" >&2
    exit 1
  }
}

log() {
  printf '\n[%s] %s\n' "$(date -u +%H:%M:%SZ)" "$*"
}

download_and_extract() {
  need curl
  need unzip
  need jq
  need docker

  if ! docker compose version >/dev/null 2>&1; then
    echo "Docker Compose v2 is required: docker compose version failed" >&2
    exit 1
  fi

  if [ "$OUTER_DIR" = "." ]; then
    log "Using current Blackboard package directory"
  elif [ ! -f "$OUTER_ZIP" ]; then
    log "Downloading Week 01 public ZIP"
    curl -fL -o "$OUTER_ZIP" "$ZIP_URL"
  else
    log "Using existing $OUTER_ZIP"
  fi

  if [ "$OUTER_DIR" = "." ]; then
    :
  elif [ ! -d "$OUTER_DIR" ]; then
    log "Extracting Blackboard ZIP"
    unzip -q "$OUTER_ZIP"
  else
    log "Using existing $OUTER_DIR"
  fi

  if [ ! -d "$OUTER_DIR/$RUNTIME_DIR" ]; then
    log "Extracting nested student runtime ZIP"
    (cd "$OUTER_DIR" && unzip -q "$RUNTIME_ZIP")
  else
    log "Using existing nested runtime directory"
  fi

  if [ ! -d "$LAB_DIR" ]; then
    echo "Expected lab directory not found: $LAB_DIR" >&2
    echo "Note: the package uses student-lab, not students-labs." >&2
    exit 1
  fi
}

wait_for_api() {
  log "Waiting for lab readiness"
  ./wait-for-lab.sh 2>&1 | tee "$EVIDENCE_REL/00-wait-for-lab.txt"
  ./check-lab.sh >"$EVIDENCE_REL/00-check-lab.txt" 2>&1 || true
  docker compose ps >"$EVIDENCE_REL/00-compose-ps-start.txt"
}

auth_token() {
  local token
  token="$(
    curl -s -X POST "$API_BASE/v1/auth/register" \
      -H "Content-Type: application/json" \
      -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\",\"full_name\":\"Lab Student\"}" |
      jq -r '.access_token // empty'
  )"

  if [ -z "$token" ] || [ "$token" = "null" ]; then
    token="$(
      curl -s -X POST "$API_BASE/v1/auth/login" \
        -H "Content-Type: application/json" \
        -d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" |
        jq -r '.access_token // empty'
    )"
  fi

  if [ -z "$token" ] || [ "$token" = "null" ]; then
    echo "Could not obtain API token" >&2
    exit 1
  fi

  TOKEN="$token"
  echo "Token acquired for $EMAIL" >"$EVIDENCE_REL/01-auth.txt"
}

api_get() {
  local path="$1"
  local output="$2"
  curl -s "$API_BASE$path" -H "Authorization: Bearer $TOKEN" >"$output"
}

start_scan() {
  local body="$1"
  local output="$2"
  curl -s -X POST "$API_BASE/v1/scanning/smart-scan" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $TOKEN" \
    -d "$body" >"$output"
  jq -r '.scan_id // empty' "$output"
}

wait_scan() {
  local scan_id="$1"
  local label="$2"
  local interval="${3:-5}"
  local max_polls="${4:-40}"
  local status="queued"

  : >"$EVIDENCE_REL/${label}-poll.log"
  for ((i = 1; i <= max_polls; i++)); do
    status="$(
      curl -sf "$API_BASE/v1/scanning/smart-scan/$scan_id/results" \
        -H "Authorization: Bearer $TOKEN" |
        jq -r '.status // .progress.phase // "queued"' 2>/dev/null || echo queued
    )"
    printf '%s poll %d: %s\n' "$label" "$i" "$status" | tee -a "$EVIDENCE_REL/${label}-poll.log"
    case "$status" in
      completed) return 0 ;;
      failed) return 1 ;;
    esac
    sleep "$interval"
  done

  echo "$label timed out with status $status" >&2
  return 1
}

run_lab() {
  mkdir -p "$EVIDENCE_REL"

  log "Starting Docker Compose lab stack"
  docker compose up -d 2>&1 | tee "$EVIDENCE_REL/00-compose-up.txt"
  wait_for_api
  auth_token

  log "Running Exercise 1"
  curl -s "$API_BASE/health" | tee "$EVIDENCE_REL/ex01-health.json" >/dev/null
  api_get "/v1/scanning/detect-range" "$EVIDENCE_REL/ex01-detect-range.json"

  log "Running Exercise 2 full scan"
  SCAN_ID="$(start_scan "{\"subnet\":\"$SUBNET\"}" "$EVIDENCE_REL/ex02-start-full-scan.json")"
  echo "SCAN_ID=$SCAN_ID" | tee "$EVIDENCE_REL/scan-ids.txt"
  wait_scan "$SCAN_ID" "ex02-full-scan" "$FULL_SCAN_INTERVAL" "$FULL_SCAN_POLLS"
  api_get "/v1/scanning/smart-scan/$SCAN_ID/results" "$EVIDENCE_REL/ex02-full-scan-results.json"
  jq '{status, hosts_discovered:(.hosts|length), duration_seconds, progress}' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex02-summary.json"

  log "Running Exercises 3 and 4 inventory summaries"
  jq '[.hosts[] | {ip, hostname, mac, vendor, open_ports}]' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex03-hosts.json"
  jq '{with_hostname:[.hosts[] | select(.hostname != null and .hostname != "")] | length,
       with_mac:[.hosts[] | select(.mac != null and .mac != "")] | length,
       with_open_ports:[.hosts[] | select((.open_ports // []) | length > 0)] | length}' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex03-richness.json"
  jq '.hosts[] | select(.ip == "172.30.0.10")' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex03-host-172.30.0.10.json"
  jq '[.hosts[] | select((.open_ports // []) | index(554)) | {ip, hostname, vendor}]' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex03-rtsp-hosts.json"
  jq '[.hosts[] | select((.open_ports // []) | index(80)) | {ip, hostname}]' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex03-http-hosts.json"
  jq '{with_mac:[.hosts[] | select(.mac != null and .mac != "")] | length,
       without_mac:[.hosts[] | select(.mac == null or .mac == "")] | length,
       total:(.hosts|length)}' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex04-mac-counts.json"
  jq '{with_hostname:[.hosts[] | select(.hostname != null and .hostname != "")] | length,
       without_hostname:[.hosts[] | select(.hostname == null or .hostname == "")] | length}' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex04-hostname-counts.json"
  jq '[.hosts[].open_ports[]] | sort | unique' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex04-unique-ports.json"
  jq '[.hosts[].open_ports[]] | group_by(.) | map({port:.[0], count:length}) | sort_by(-.count)' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex04-port-frequency.json"

  log "Running Exercise 5 SSE stream"
  STREAM_SCAN="$(start_scan "{\"subnet\":\"$SUBNET\",\"discovery_only\":true}" "$EVIDENCE_REL/ex05-start-stream-scan.json")"
  echo "STREAM_SCAN=$STREAM_SCAN" >>"$EVIDENCE_REL/scan-ids.txt"
  (curl -sN --max-time 30 "$API_BASE/v1/scanning/stream/$STREAM_SCAN" \
    -H "Authorization: Bearer $TOKEN" || [ "$?" -eq 28 ]) >"$EVIDENCE_REL/ex05-sse-events.txt"
  wait_scan "$STREAM_SCAN" "ex05-stream-scan" 5 "$DISCOVERY_SCAN_POLLS"
  api_get "/v1/scanning/smart-scan/$STREAM_SCAN/results" "$EVIDENCE_REL/ex05-stream-scan-results.json"

  log "Running Exercises 6 and 7 discovery-only and passive/active summaries"
  DISCO_SCAN="$(start_scan "{\"subnet\":\"$SUBNET\",\"discovery_only\":true}" "$EVIDENCE_REL/ex06-start-discovery-only.json")"
  echo "DISCO_SCAN=$DISCO_SCAN" >>"$EVIDENCE_REL/scan-ids.txt"
  wait_scan "$DISCO_SCAN" "ex06-discovery-only" 5 "$DISCOVERY_SCAN_POLLS"
  api_get "/v1/scanning/smart-scan/$DISCO_SCAN/results" "$EVIDENCE_REL/ex06-discovery-only-results.json"
  jq '{hosts:(.hosts|length), duration_seconds}' "$EVIDENCE_REL/ex06-discovery-only-results.json" >"$EVIDENCE_REL/ex06-discovery-summary.json"
  jq '{hosts:(.hosts|length), duration_seconds}' "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex06-full-summary.json"
  jq '[.hosts[] | select(.mac != null) | {ip, mac, hostname}] | length' "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex07-arp-count.json"
  jq '{hosts_with_hostname:[.hosts[] | select(.hostname != null and .hostname != "")] | length,
       hosts_without_hostname:[.hosts[] | select(.hostname == null or .hostname == "")] | length}' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex07-hostname-counts.json"
  jq '{with_open_ports:[.hosts[] | select((.open_ports // []) | length > 0)] | length,
       without_open_ports:[.hosts[] | select((.open_ports // []) | length == 0)] | length}' \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex07-open-port-counts.json"

  log "Running Exercise 8 baseline save and compare"
  jq "{subnet:\"$SUBNET\", hosts:.hosts}" "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex08-baseline-save-payload.json"
  curl -s -X POST "$API_BASE/v1/scanning/baseline/save" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $TOKEN" \
    --data-binary @"$EVIDENCE_REL/ex08-baseline-save-payload.json" >"$EVIDENCE_REL/ex08-baseline-save-response.json"
  NEW_SCAN="$(start_scan "{\"subnet\":\"$SUBNET\",\"discovery_only\":true}" "$EVIDENCE_REL/ex08-start-new-scan.json")"
  echo "NEW_SCAN=$NEW_SCAN" >>"$EVIDENCE_REL/scan-ids.txt"
  wait_scan "$NEW_SCAN" "ex08-new-scan" 10 "$DISCOVERY_SCAN_POLLS"
  api_get "/v1/scanning/smart-scan/$NEW_SCAN/results" "$EVIDENCE_REL/ex08-new-scan-results.json"
  jq "{subnet:\"$SUBNET\", hosts:.hosts}" "$EVIDENCE_REL/ex08-new-scan-results.json" >"$EVIDENCE_REL/ex08-baseline-compare-payload.json"
  curl -s -X POST "$API_BASE/v1/scanning/baseline/compare" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $TOKEN" \
    --data-binary @"$EVIDENCE_REL/ex08-baseline-compare-payload.json" >"$EVIDENCE_REL/ex08-baseline-compare-response.json"
  api_get "/v1/scanning/baselines" "$EVIDENCE_REL/ex08-baselines-list.json"

  log "Running Exercise 9 history and schedule"
  api_get "/v1/scanning/smart-scan/history?limit=10" "$EVIDENCE_REL/ex09-history.json"
  jq '[.scans[] | select(.status == "completed") | {scan_id:.scan_id[:12], subnet, hosts_discovered, duration_seconds}]' \
    "$EVIDENCE_REL/ex09-history.json" >"$EVIDENCE_REL/ex09-history-completed-summary.json"
  curl -s -X POST "$API_BASE/v1/scanning/schedules" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $TOKEN" \
    -d "{\"subnet\":\"$SUBNET\",\"cron_expression\":\"0 2 * * *\",\"name\":\"Daily IoT Inventory\"}" >"$EVIDENCE_REL/ex09-schedule-create.json"
  api_get "/v1/scanning/schedules" "$EVIDENCE_REL/ex09-schedules-list.json"
  jq '.data.schedules[] | {name, subnet, cron_expression, next_run_time, is_active}' \
    "$EVIDENCE_REL/ex09-schedules-list.json" >"$EVIDENCE_REL/ex09-schedules-summary.json"

  log "Running Exercise 10 report generation"
  curl -s "$API_BASE/v1/reports/$SCAN_ID/html" -H "Authorization: Bearer $TOKEN" -o "$EVIDENCE_REL/ex10-discovery-report.html"
  curl -s "$API_BASE/v1/reports/$SCAN_ID/pdf" -H "Authorization: Bearer $TOKEN" -o "$EVIDENCE_REL/ex10-discovery-report.pdf"
  {
    echo 'IP,MAC,Hostname,Vendor Hint,Open Ports'
    jq -r '.hosts[] | [.ip, (.mac // "unknown"), (.hostname // "unknown"), (.vendor // "unknown"),
      ((.open_ports // []) | map(tostring) | join(";"))] | @csv' "$EVIDENCE_REL/ex02-full-scan-results.json"
  } >"$EVIDENCE_REL/ex10-inventory.csv"
  jq "{scan_id:.scan_id, subnet:\"$SUBNET\", scan_date:.started_at, total_hosts:(.hosts|length),
       unique_vendors:([.hosts[].vendor // \"unknown\"] | unique | length),
       vendor_hints:([.hosts[].vendor // \"unknown\"] | group_by(.) | map({vendor:.[0], count:length})),
       hosts_with_hostname:([.hosts[] | select(.hostname != null and .hostname != \"\")] | length),
       hosts_with_http:([.hosts[] | select((.open_ports // []) | index(80))] | length),
       hosts_with_ssh:([.hosts[] | select((.open_ports // []) | index(22))] | length),
       hosts_with_rtsp:([.hosts[] | select((.open_ports // []) | index(554))] | length)}" \
    "$EVIDENCE_REL/ex02-full-scan-results.json" >"$EVIDENCE_REL/ex10-audit-summary.json"
}

write_html_report() {
  local report="$RESULTS_REL/index.html"
  local full_hosts disco_hosts vendors http_hosts ssh_hosts rtsp_hosts
  local baseline_hosts current_hosts new_count missing_count changed_count
  local pdf_status html_size pdf_size cleanup_status

  full_hosts="$(jq -r '.total_hosts' "$EVIDENCE_REL/ex10-audit-summary.json")"
  vendors="$(jq -r '.unique_vendors' "$EVIDENCE_REL/ex10-audit-summary.json")"
  http_hosts="$(jq -r '.hosts_with_http' "$EVIDENCE_REL/ex10-audit-summary.json")"
  ssh_hosts="$(jq -r '.hosts_with_ssh' "$EVIDENCE_REL/ex10-audit-summary.json")"
  rtsp_hosts="$(jq -r '.hosts_with_rtsp' "$EVIDENCE_REL/ex10-audit-summary.json")"
  disco_hosts="$(jq -r '.hosts' "$EVIDENCE_REL/ex06-discovery-summary.json")"
  baseline_hosts="$(jq -r '.baseline_host_count // "n/a"' "$EVIDENCE_REL/ex08-baseline-compare-response.json")"
  current_hosts="$(jq -r '.current_host_count // "n/a"' "$EVIDENCE_REL/ex08-baseline-compare-response.json")"
  new_count="$(jq -r '.new_count // (.new_hosts | length) // "n/a"' "$EVIDENCE_REL/ex08-baseline-compare-response.json")"
  missing_count="$(jq -r '.missing_count // (.missing_hosts | length) // "n/a"' "$EVIDENCE_REL/ex08-baseline-compare-response.json")"
  changed_count="$(jq -r '.changed_count // (.changed_hosts | length) // "n/a"' "$EVIDENCE_REL/ex08-baseline-compare-response.json")"
  html_size="$(wc -c <"$EVIDENCE_REL/ex10-discovery-report.html" | tr -d ' ')"
  pdf_size="$(wc -c <"$EVIDENCE_REL/ex10-discovery-report.pdf" | tr -d ' ')"
  if file "$EVIDENCE_REL/ex10-discovery-report.pdf" | grep -qi 'PDF document'; then
    pdf_status="PDF report generated (${pdf_size} bytes)"
  else
    pdf_status="PDF endpoint did not return a PDF; body preserved (${pdf_size} bytes)"
  fi
  cleanup_status="Pending when report first written; see cleanup evidence after script exits."

  cat >"$report" <<HTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>SEAS-8414 Week 01 Automated Results</title>
  <style>
    body { font-family: Arial, sans-serif; line-height: 1.45; color: #172033; margin: 32px; max-width: 1080px; }
    h1, h2 { color: #0f2742; }
    h2 { margin-top: 28px; border-bottom: 1px solid #cfd7e3; padding-bottom: 4px; }
    table { border-collapse: collapse; width: 100%; margin: 12px 0 20px; }
    th, td { border: 1px solid #cfd7e3; padding: 8px; vertical-align: top; }
    th { background: #eef3f8; text-align: left; }
    code { background: #eef3f8; padding: 1px 4px; border-radius: 3px; }
    .pass { color: #137333; font-weight: 700; }
    .warn { color: #9a5b00; font-weight: 700; }
  </style>
</head>
<body>
  <h1>SEAS-8414 Week 01: Network Discovery and Asset Inventory</h1>
  <p>Generated by <code>run-week01-lab.sh</code>. The corrected runnable directory is <code>$LAB_DIR</code>; the public package contains <code>student-lab</code>, not <code>students-labs</code>.</p>

  <h2>Environment</h2>
  <table>
    <tr><th>Item</th><th>Observed result</th><th>Evidence</th></tr>
    <tr><td>API health</td><td>Health endpoint collected successfully.</td><td><a href="evidence/ex01-health.json">ex01-health.json</a></td></tr>
    <tr><td>Detected subnet</td><td><code>$SUBNET</code></td><td><a href="evidence/ex01-detect-range.json">ex01-detect-range.json</a></td></tr>
    <tr><td>Compose readiness</td><td>Compose stack started and readiness scripts ran.</td><td><a href="evidence/00-wait-for-lab.txt">wait log</a>, <a href="evidence/00-compose-ps-start.txt">compose ps</a></td></tr>
  </table>

  <h2>Task Status</h2>
  <table>
    <tr><th>Task</th><th>Status</th><th>Observed result</th><th>Evidence</th></tr>
    <tr><td>Download and extraction</td><td class="pass">Pass</td><td>Outer ZIP and nested runtime ZIP available.</td><td>Script transcript in terminal; package files on disk.</td></tr>
    <tr><td>Full progressive scan</td><td class="pass">Pass</td><td>Completed with $full_hosts admitted hosts.</td><td><a href="evidence/ex02-summary.json">summary</a>, <a href="evidence/ex02-full-scan-results.json">raw JSON</a></td></tr>
    <tr><td>Host inventory</td><td class="pass">Pass</td><td>CSV inventory and host JSON generated.</td><td><a href="evidence/ex10-inventory.csv">CSV</a>, <a href="evidence/ex03-hosts.json">host JSON</a></td></tr>
    <tr><td>Discovery method summaries</td><td class="pass">Pass</td><td>Hostname, MAC, and port distribution summaries generated.</td><td><a href="evidence/ex04-port-frequency.json">port frequency</a>, <a href="evidence/ex04-mac-counts.json">MAC counts</a></td></tr>
    <tr><td>SSE stream</td><td class="pass">Pass</td><td>Bounded event stream captured.</td><td><a href="evidence/ex05-sse-events.txt">SSE events</a></td></tr>
    <tr><td>Discovery-only scan</td><td class="pass">Pass</td><td>Completed with $disco_hosts admitted hosts.</td><td><a href="evidence/ex06-discovery-summary.json">summary</a></td></tr>
    <tr><td>Baseline save and compare</td><td class="pass">Pass</td><td>Baseline $baseline_hosts hosts; current $current_hosts hosts; new $new_count, missing $missing_count, changed $changed_count.</td><td><a href="evidence/ex08-baseline-compare-response.json">compare JSON</a></td></tr>
    <tr><td>History and schedule</td><td class="pass">Pass</td><td>History summary and daily 02:00 UTC schedule request captured.</td><td><a href="evidence/ex09-history-completed-summary.json">history</a>, <a href="evidence/ex09-schedule-create.json">schedule</a></td></tr>
    <tr><td>HTML/PDF report endpoints</td><td class="warn">HTML pass; PDF checked</td><td>HTML report saved ($html_size bytes). $pdf_status.</td><td><a href="evidence/ex10-discovery-report.html">HTML report</a>, <a href="evidence/ex10-discovery-report.pdf">PDF response</a></td></tr>
    <tr><td>Cleanup</td><td class="pass">Pass after script exit</td><td>$cleanup_status</td><td><a href="evidence/99-cleanup.txt">cleanup log</a></td></tr>
  </table>

  <h2>Inventory Summary</h2>
  <table>
    <tr><th>Metric</th><th>Observed value</th></tr>
    <tr><td>Full scan ID</td><td><code>$SCAN_ID</code></td></tr>
    <tr><td>Full scan admitted hosts</td><td>$full_hosts</td></tr>
    <tr><td>Discovery-only admitted hosts</td><td>$disco_hosts</td></tr>
    <tr><td>Unique vendor hints</td><td>$vendors</td></tr>
    <tr><td>Hosts with HTTP</td><td>$http_hosts</td></tr>
    <tr><td>Hosts with SSH</td><td>$ssh_hosts</td></tr>
    <tr><td>Hosts with RTSP</td><td>$rtsp_hosts</td></tr>
  </table>

  <h2>Notes</h2>
  <p>This report records observed evidence, not expected sample output. If host counts or MAC fields differ from the PDF examples, the JSON evidence files are the source of truth for this run.</p>
</body>
</html>
HTML
}

cleanup_lab() {
  if [ "$DRY_RUN" = "1" ]; then
    return 0
  fi
  local abs_lab_dir="$ROOT_DIR/$LAB_DIR"
  if [ ! -d "$abs_lab_dir" ]; then
    return 0
  fi

  cd "$abs_lab_dir"
  mkdir -p "$EVIDENCE_REL"

  if [ "$KEEP_LAB" = "1" ]; then
    echo "KEEP_LAB=1 set; compose stack left running for debugging." >"$EVIDENCE_REL/99-cleanup.txt"
    return 0
  fi

  {
    echo '$ docker compose down -v --remove-orphans'
    docker compose down -v --remove-orphans
    echo
    echo '$ docker compose ps'
    docker compose ps
  } >"$EVIDENCE_REL/99-cleanup.txt" 2>&1 || true

  if [ -f "$RESULTS_REL/index.html" ]; then
    python3 - "$RESULTS_REL/index.html" "$EVIDENCE_REL/99-cleanup.txt" <<'PY'
from pathlib import Path
report = Path(__import__("sys").argv[1])
cleanup = Path(__import__("sys").argv[2])
text = report.read_text()
status = "Cleanup completed; compose stack and volume removal attempted. See cleanup evidence."
if cleanup.exists() and "NAME      IMAGE" in cleanup.read_text():
    status = "Cleanup completed; final docker compose ps was empty."
text = text.replace("Pending when report first written; see cleanup evidence after script exits.", status)
report.write_text(text)
PY
  fi
}

main() {
  trap cleanup_lab EXIT
  download_and_extract

  cd "$LAB_DIR"
  mkdir -p "$EVIDENCE_REL"

  {
    echo "Started: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
    echo "Root: $ROOT_DIR"
    echo "Lab dir: $LAB_DIR"
    echo "Subnet: $SUBNET"
    echo "Note: this package uses student-lab, not students-labs."
  } >"$EVIDENCE_REL/00-run-metadata.txt"

  if [ "$DRY_RUN" = "1" ]; then
    log "Dry run complete"
    echo "$ROOT_DIR/$LAB_DIR"
    return 0
  fi

  run_lab
  write_html_report

  log "Report generated"
  echo "$ROOT_DIR/$LAB_DIR/$RESULTS_REL/index.html"

  log "Verifying report links"
  if [ ! -f "$EVIDENCE_REL/99-cleanup.txt" ]; then
    echo "Cleanup pending; final cleanup evidence is written at script exit." >"$EVIDENCE_REL/99-cleanup.txt"
  fi
  missing=0
  for href in $(grep -Eo 'href="[^"]+"' "$RESULTS_REL/index.html" | sed 's/^href="//; s/"$//' | grep '^evidence/'); do
    if [ ! -e "$RESULTS_REL/$href" ]; then
      echo "Missing linked evidence: $href" >&2
      missing=1
    fi
  done
  if [ "$missing" -ne 0 ]; then
    exit 1
  fi
  echo "All report evidence links resolved." >"$EVIDENCE_REL/98-html-verification.txt"
}

main "$@"
