#!/usr/bin/env bash
# CatchupWeb Estate Audit v1.0
# Read-only discovery for Open Line quoting.
# Does not install packages, change config, open ports, or scan the network.
# Review this file, then:  sudo bash estate-audit.sh
# Optional:  sudo bash estate-audit.sh --out /root/cw-audit
set -u

CW_VER="1.0"
OUT_DIR=""
QUIET=0
while [ $# -gt 0 ]; do
  case "$1" in
    --out) OUT_DIR="${2:-}"; shift 2 ;;
    --quiet|-q) QUIET=1; shift ;;
    -h|--help)
      echo "Usage: sudo bash estate-audit.sh [--out DIR] [--quiet]"
      echo "Read-only. Writes JSON + quote sheet. No packages installed."
      exit 0 ;;
    *) echo "Unknown option: $1" >&2; exit 2 ;;
  esac
done

HOST="$(hostname -f 2>/dev/null || hostname)"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
if [ -z "$OUT_DIR" ]; then
  OUT_DIR="/tmp/cw-estate-audit-${HOST%%.*}-${STAMP}"
fi
mkdir -p "$OUT_DIR"
chmod 700 "$OUT_DIR"
JSON="$OUT_DIR/estate.json"
QUOTE="$OUT_DIR/quote.txt"
RAW="$OUT_DIR/raw.txt"

log() { [ "$QUIET" = 1 ] || printf '%s\n' "$*"; }
have() { command -v "$1" >/dev/null 2>&1; }

# Bound every probe. Never hang a production box.
run() {
  if have timeout; then
    timeout 4 "$@" 2>/dev/null || true
  else
    "$@" 2>/dev/null || true
  fi
}

esc() {
  if command -v python3 >/dev/null 2>&1; then
    printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()), end="")'
  else
    printf '"%s"' "$(printf '%s' "$1" | sed 's/[\\"]/\\&/g')"
  fi
}

{
  echo "=== CatchupWeb Estate Audit $CW_VER ==="
  echo "host=$HOST stamp=$STAMP user=$(id -un) uid=$(id -u)"
} > "$RAW"

# ── identity ──────────────────────────────────────────────────────
OS_ID="$(run awk -F= '/^ID=/{gsub(/"/,"");print $2}' /etc/os-release)"
OS_VER="$(run awk -F= '/^VERSION_ID=/{gsub(/"/,"");print $2}' /etc/os-release)"
OS_PRETTY="$(run awk -F= '/^PRETTY_NAME=/{gsub(/"/,"");print $2}' /etc/os-release)"
KERNEL="$(run uname -r)"
ARCH="$(run uname -m)"
VIRT="$(run systemd-detect-virt)"
[ -z "$VIRT" ] && VIRT="unknown"
if [ -f /.dockerenv ] || grep -qE 'docker|lxc|kubepod' /proc/1/cgroup 2>/dev/null; then
  VIRT="container"
fi

# ── capacity ──────────────────────────────────────────────────────
CORES="$(run nproc)"
[ -z "$CORES" ] && CORES="$(run awk '/^processor/{c++} END{print c+0}' /proc/cpuinfo)"
MEM_KB="$(run awk '/MemTotal/{print $2}' /proc/meminfo)"
MEM_GB="$(awk -v k="${MEM_KB:-0}" 'BEGIN{printf "%.1f", k/1024/1024}')"
SWAP_KB="$(run awk '/SwapTotal/{print $2}' /proc/meminfo)"
SWAP_GB="$(awk -v k="${SWAP_KB:-0}" 'BEGIN{printf "%.1f", k/1024/1024}')"
LOAD="$(run awk '{print $1","$2","$3}' /proc/loadavg)"
UPTIME_S="$(run awk '{print int($1)}' /proc/uptime)"

DISK_JSON="["
FIRST=1
while read -r fs size used avail pct mnt; do
  case "$mnt" in /snap*|/boot/efi|/run*|/dev*|/sys*|/proc*) continue ;; esac
  [ "$FIRST" = 1 ] || DISK_JSON+=","
  FIRST=0
  DISK_JSON+=$(printf '{"mount":%s,"size":%s,"used":%s,"avail":%s,"pct":%s}' \
    "$(esc "$mnt")" "$(esc "$size")" "$(esc "$used")" "$(esc "$avail")" "$(esc "$pct")")
done < <(run df -hP -x tmpfs -x devtmpfs -x squashfs -x overlay | awk 'NR>1{print $1,$2,$3,$4,$5,$6}')
DISK_JSON+="]"

# ── packages / services ───────────────────────────────────────────
PKG_MGR="none"
PKG_COUNT=0
if have dpkg-query; then
  PKG_MGR="apt"
  PKG_COUNT="$(run dpkg-query -W | wc -l | tr -d ' ')"
elif have rpm; then
  PKG_MGR="rpm"
  PKG_COUNT="$(run rpm -qa | wc -l | tr -d ' ')"
elif have pacman; then
  PKG_MGR="pacman"
  PKG_COUNT="$(run pacman -Q | wc -l | tr -d ' ')"
fi

LAST_PATCH="unknown"
if [ -f /var/log/apt/history.log ]; then
  LAST_PATCH="$(run awk '/^Start-Date:/{d=$2} END{print d}' /var/log/apt/history.log)"
elif [ -f /var/log/dnf.log ]; then
  LAST_PATCH="$(run tail -n 5 /var/log/dnf.log | head -n 1 | awk '{print $1}')"
elif [ -f /var/log/yum.log ]; then
  LAST_PATCH="$(run tail -n 1 /var/log/yum.log | awk '{print $1,$2,$3}')"
fi

svc_active() {
  local u="$1"
  if have systemctl; then
    run systemctl is-active "$u" | grep -qx active && return 0
  fi
  pgrep -x "$u" >/dev/null 2>&1
}

bin_present() { have "$1"; }

detect() {
  # name: returns 1 if found
  local name="$1"; shift
  local x
  for x in "$@"; do
    case "$x" in
      svc:*) svc_active "${x#svc:}" && return 0 ;;
      bin:*) bin_present "${x#bin:}" && return 0 ;;
      path:*) [ -e "${x#path:}" ] && return 0 ;;
    esac
  done
  return 1
}

WP_COUNT=0
if have find; then
  WP_COUNT="$(run find /var/www /srv /home /opt -maxdepth 5 -type f -name wp-config.php 2>/dev/null | wc -l | tr -d ' ')"
fi
[ -z "$WP_COUNT" ] && WP_COUNT=0

OLLAMA=0
detect ollama svc:ollama bin:ollama path:/usr/local/bin/ollama && OLLAMA=1
pgrep -f '[o]llama serve' >/dev/null 2>&1 && OLLAMA=1

K8S=0
detect k8s svc:kubelet bin:kubectl path:/etc/kubernetes/admin.conf && K8S=1

DOCKER=0
detect docker svc:docker bin:docker svc:containerd && DOCKER=1
CTR=0
if [ "$DOCKER" = 1 ] && have docker; then
  CTR="$(run docker ps -q | wc -l | tr -d ' ')"
fi
[ -z "$CTR" ] && CTR=0

CEPH=0
detect ceph bin:ceph svc:ceph-mon.target svc:ceph-osd.target && CEPH=1

MAIL=0
detect mail svc:postfix svc:dovecot bin:postfix && MAIL=1

NGINX=0; APACHE=0; PG=0; MYSQL=0; PROM=0; GRAF=0
detect nginx svc:nginx bin:nginx && NGINX=1
detect apache svc:apache2 svc:httpd bin:apache2 bin:httpd && APACHE=1
detect postgres svc:postgresql bin:psql path:/var/lib/postgresql && PG=1
run ss -lntH | grep -qE ':5432$' && PG=1
detect mysql svc:mysql svc:mariadb bin:mysqld && MYSQL=1
detect prom svc:prometheus bin:prometheus && PROM=1
detect graf svc:grafana-server bin:grafana-server && GRAF=1

LISTEN="$(run ss -lntH | awk '{n=$4; sub(/.*:/,"",n); if (n ~ /^[0-9]+$/) print n}' | sort -n | uniq | tr '\n' ',' | sed 's/,$//')"
if [ -z "$LISTEN" ]; then
  LISTEN="$(run ss -lntH | awk '/LISTEN/ {n=$5; sub(/.*:/,"",n); if (n ~ /^[0-9]+$/) print n}' | sort -n | uniq | tr '\n' ',' | sed 's/,$//')"
fi

UNITS_N=0
if have systemctl; then
  UNITS_N="$(run systemctl list-units --type=service --state=running --no-legend --no-pager | wc -l | tr -d ' ')"
fi

REBOOT_REQ=0
[ -f /var/run/reboot-required ] && REBOOT_REQ=1

# ── quote map (Open Line catalogue, starting from) ────────────────
NODES=1
INBOX=()
ADDONS=()
DISTRO_CLASS="other"
case "$OS_ID" in
  ubuntu|debian|raspbian|linuxmint) DISTRO_CLASS="debian-family" ;;
  rhel|centos|rocky|almalinux|fedora|ol) DISTRO_CLASS="rhel-family" ;;
  sles|opensuse*) DISTRO_CLASS="suse-family" ;;
  *)
    if grep -qi 'raspberry' /proc/device-tree/model 2>/dev/null || grep -qi raspberry /etc/os-release 2>/dev/null; then
      DISTRO_CLASS="pi"
      OS_ID="${OS_ID:-raspios}"
    fi
    ;;
esac
if grep -qiE 'raspberry|orange.?pi|armbian' /etc/os-release 2>/dev/null /proc/device-tree/model 2>/dev/null; then
  DISTRO_CLASS="edge"
fi

[ "$NGINX" = 1 ] || [ "$APACHE" = 1 ] && INBOX+=("web")
[ "$PG" = 1 ] || [ "$MYSQL" = 1 ] && INBOX+=("database")
[ "$DOCKER" = 1 ] && INBOX+=("docker")
[ "$PROM" = 1 ] || [ "$GRAF" = 1 ] && INBOX+=("observability")

[ "$WP_COUNT" -gt 0 ] && ADDONS+=("wordpress:${WP_COUNT}")
[ "$MAIL" = 1 ] && ADDONS+=("mail")
[ "$K8S" = 1 ] && ADDONS+=("kubernetes")
[ "$OLLAMA" = 1 ] && ADDONS+=("private-ai")
[ "$CEPH" = 1 ] && ADDONS+=("ceph")
[ "$DISTRO_CLASS" = "edge" ] || [ "$DISTRO_CLASS" = "pi" ] && ADDONS+=("edge-device")

# Starting-from Open Line prices (ZAR, excl VAT) — single host hint
BASE_BENCH=4950
BASE_WORK=9850
BASE_LINE=19500
ADD_WP=1450
ADD_MAIL=1950
ADD_K8S=4950
ADD_AI=4500
ADD_EDGE=2950
ADD_CEPH=6500

ADD_TOTAL=0
HINT=""
for a in "${ADDONS[@]+"${ADDONS[@]}"}"; do
  case "$a" in
    wordpress:*) HINT+="wordpress "; ADD_TOTAL=$((ADD_TOTAL+ADD_WP)) ;;
    mail) HINT+="mail "; ADD_TOTAL=$((ADD_TOTAL+ADD_MAIL)) ;;
    kubernetes) HINT+="k8s "; ADD_TOTAL=$((ADD_TOTAL+ADD_K8S)) ;;
    private-ai) HINT+="private-ai "; ADD_TOTAL=$((ADD_TOTAL+ADD_AI)) ;;
    edge-device) HINT+="edge "; ADD_TOTAL=$((ADD_TOTAL+ADD_EDGE)) ;;
    ceph) HINT+="ceph "; ADD_TOTAL=$((ADD_TOTAL+ADD_CEPH)) ;;
  esac
done

# Single node almost always Bench unless k8s/ai/ceph
SUGGEST="Open Bench"
SUGGEST_ZAR=$((BASE_BENCH+ADD_TOTAL))
if [ "$K8S" = 1 ] || [ "$OLLAMA" = 1 ] || [ "$CEPH" = 1 ]; then
  SUGGEST="Open Workshop"
  SUGGEST_ZAR=$((BASE_WORK+ADD_TOTAL))
  # Workshop already includes one k8s cluster — don't double-count
  if [ "$K8S" = 1 ]; then
    SUGGEST_ZAR=$((SUGGEST_ZAR-ADD_K8S))
    HINT="${HINT//k8s /k8s(included-on-workshop) }"
  fi
fi

INBOX_CSV="$(IFS=,; echo "${INBOX[*]-}")"
ADDONS_CSV="$(IFS=,; echo "${ADDONS[*]-}")"

# ── JSON ──────────────────────────────────────────────────────────
{
  printf '{\n'
  printf '  "tool": "catchupweb-estate-audit",\n'
  printf '  "version": %s,\n' "$(esc "$CW_VER")"
  printf '  "generated_utc": %s,\n' "$(esc "$STAMP")"
  printf '  "hostname": %s,\n' "$(esc "$HOST")"
  printf '  "user": %s,\n' "$(esc "$(id -un)")"
  printf '  "privileged": %s,\n' "$([ "$(id -u)" = 0 ] && echo true || echo false)"
  printf '  "os": {"id":%s,"version":%s,"pretty":%s,"kernel":%s,"arch":%s,"virt":%s,"class":%s},\n' \
    "$(esc "$OS_ID")" "$(esc "$OS_VER")" "$(esc "$OS_PRETTY")" "$(esc "$KERNEL")" "$(esc "$ARCH")" "$(esc "$VIRT")" "$(esc "$DISTRO_CLASS")"
  printf '  "capacity": {"cores":%s,"mem_gb":%s,"swap_gb":%s,"load":%s,"uptime_s":%s},\n' \
    "$(esc "${CORES:-0}")" "$(esc "$MEM_GB")" "$(esc "$SWAP_GB")" "$(esc "$LOAD")" "$(esc "${UPTIME_S:-0}")"
  printf '  "disk": %s,\n' "$DISK_JSON"
  printf '  "packages": {"manager":%s,"count":%s,"last_patch":%s,"reboot_required":%s},\n' \
    "$(esc "$PKG_MGR")" "$(esc "${PKG_COUNT:-0}")" "$(esc "$LAST_PATCH")" "$([ "$REBOOT_REQ" = 1 ] && echo true || echo false)"
  printf '  "services": {"running_units":%s,"listen_ports":%s},\n' "$(esc "${UNITS_N:-0}")" "$(esc "$LISTEN")"
  printf '  "stack": {\n'
  printf '    "nginx": %s, "apache": %s, "postgresql": %s, "mysql": %s,\n' \
    "$([ "$NGINX" = 1 ] && echo true || echo false)" \
    "$([ "$APACHE" = 1 ] && echo true || echo false)" \
    "$([ "$PG" = 1 ] && echo true || echo false)" \
    "$([ "$MYSQL" = 1 ] && echo true || echo false)"
  printf '    "docker": %s, "docker_containers": %s, "kubernetes": %s,\n' \
    "$([ "$DOCKER" = 1 ] && echo true || echo false)" "$(esc "$CTR")" \
    "$([ "$K8S" = 1 ] && echo true || echo false)"
  printf '    "wordpress_sites": %s, "mail": %s, "prometheus": %s, "grafana": %s,\n' \
    "$(esc "$WP_COUNT")" \
    "$([ "$MAIL" = 1 ] && echo true || echo false)" \
    "$([ "$PROM" = 1 ] && echo true || echo false)" \
    "$([ "$GRAF" = 1 ] && echo true || echo false)"
  printf '    "ollama": %s, "ceph": %s\n' \
    "$([ "$OLLAMA" = 1 ] && echo true || echo false)" \
    "$([ "$CEPH" = 1 ] && echo true || echo false)"
  printf '  },\n'
  printf '  "open_line": {\n'
  printf '    "inbox": %s,\n' "$(esc "$INBOX_CSV")"
  printf '    "addons": %s,\n' "$(esc "$ADDONS_CSV")"
  printf '    "suggested_package": %s,\n' "$(esc "$SUGGEST")"
  printf '    "starting_from_zar": %s,\n' "$SUGGEST_ZAR"
  printf '    "currency": "ZAR",\n'
  printf '    "note": "Single-host hint. Fleet merge required for estate quote. Prices starting from, excl VAT."\n'
  printf '  }\n'
  printf '}\n'
} > "$JSON"

# ── quote sheet ───────────────────────────────────────────────────
{
  echo "CatchupWeb Open Line — quote sheet (single host)"
  echo "Generated: $STAMP   Host: $HOST"
  echo "------------------------------------------------"
  echo "OS:       ${OS_PRETTY:-$OS_ID $OS_VER}  ($DISTRO_CLASS)"
  echo "Kernel:   $KERNEL  arch=$ARCH  virt=$VIRT"
  echo "Capacity: ${CORES} cores  ${MEM_GB}G RAM  ${SWAP_GB}G swap  load=$LOAD"
  echo "Disk:"
  run df -hP -x tmpfs -x devtmpfs -x squashfs | awk 'NR==1 || $6 !~ /^\/(snap|run|sys|proc|dev)/'
  echo "Packages: $PKG_MGR count=$PKG_COUNT last_patch=$LAST_PATCH reboot_required=$REBOOT_REQ"
  echo "Listen:   $LISTEN"
  echo "In-box:   ${INBOX_CSV:-none}"
  echo "Add-ons:  ${ADDONS_CSV:-none}"
  echo "------------------------------------------------"
  echo "Suggested (this host only, starting from, excl VAT):"
  echo "  $SUGGEST  starting from R${SUGGEST_ZAR}"
  echo "  Extra hours R1,650 · 10-hour pack R11,500"
  echo "  Merge all host JSON files before issuing an estate quote."
  echo "  Catalogue: https://www.catchupweb.co.za/open-line/"
  echo "------------------------------------------------"
  echo "This run was read-only. No packages installed, no config changed."
} > "$QUOTE"

cp "$JSON" "$RAW.json" 2>/dev/null || true
cat "$QUOTE" >> "$RAW"

log "CatchupWeb estate audit $CW_VER — read-only"
log "Wrote:"
log "  $JSON"
log "  $QUOTE"
log ""
cat "$QUOTE"
log ""
log "Send both files to CatchupWeb (redact hostnames if needed)."
log "Fleet: run on each node, then bash estate-audit-merge.sh reports/*.json"

exit 0
