#!/usr/bin/env bash
# H7 SECURE LEARN · free-ball local security learning
# Learn: PIDs · services · daemons · grok processes · file change heuristics
# Goal: keep INSIDE in · OUTSIDE out · no commercial AV · no free-ride cloud
#
#   ./Build/h7-secure-learn.sh baseline|learn|diff|status|once
#
# Law: BGS→BGF→BGL→SDF · SPV free · C IS LIE · Always Hostess 7 · remain IN
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="$ROOT/out"
SEC="$OUT/h7secure"
BASE="$SEC/baseline"
NOW="$SEC/now"
LEARN_JSON="$OUT/h7_secure_learn.json"
HEUR_TSV="$OUT/h7_secure_heuristics.tsv"
FEED="$OUT/h7_vocab_feed.tsv"
cmd="${1:-once}"
TS="$(date -Iseconds)"

# INSIDE roots (trust boundary · desk self)
INSIDE_ROOTS=(
  "$ROOT"
  "$HOME/Projects/x"
  "$HOME/Projects/KateRTX"
  "$HOME/Projects/biggrinrtx-magazine"
  "$HOME/Projects/GrokClaws"
  "$HOME/Projects/grok-employee"
  "$HOME/Desktop/BigGrinStorage"
  "$HOME/.grok"
  "$HOME/.config/biggrinrtx"
  "$HOME/.config/grok-claws"
  "/var/www/grok-employee"
)

# OUTSIDE signals (public thrash / untrusted)
# Note: Apache on own 80/443 is desk legit · not thrash (see h7-desk-ports.lib.sh)
OUTSIDE_HINTS='0.0.0.0:|::|nginx|node |docker|snapd|cups-browsed|avahi|rpcbind|telnet|vsftpd'

# shellcheck source=h7-desk-ports.lib.sh
. "$(cd "$(dirname "$0")" && pwd)/h7-desk-ports.lib.sh"

mkdir -p "$BASE" "$NOW" "$OUT"
say() { echo "[$TS] $*"; }

# count helper · never double-zero (grep -c || echo 0 → "00")
n0() { local v="${1//[^0-9]/}"; echo "${v:-0}"; }

# thrash / kernel noise · not real OUT? process classes
is_thrash_comm() {
  case "$1" in
    kworker/*|ksoftirqd/*|migration/*|rcu_*|idle_inject/*|cpuhp/*|watchdog/*|kswapd*|kcompactd*|kblockd|kauditd|kdevtmpfs|khungtaskd|oom_reaper|writeback|kthreadd|irq/*|scsi_*|ext4-*|jbd2/*|card*-crtc*|amdgpu*|nv_queue|nvidia-*|UVM*|systemd-journal|dbus-daemon) return 0 ;;
  esac
  return 1
}

# ── capture live world (fast) ──
capture() {
  local dest="$1"
  mkdir -p "$dest"
  # PIDs: pid user comm args (truncated)
  ps -eo pid=,user=,comm=,args= --sort=pid 2>/dev/null \
    | awk '{
        pid=$1; user=$2; comm=$3;
        $1=$2=$3=""; sub(/^ +/,"");
        args=$0; if(length(args)>120) args=substr(args,1,120);
        gsub(/\t/," ",args);
        printf "%s\t%s\t%s\t%s\n", pid, user, comm, args
      }' >"$dest/pids.tsv" || : >"$dest/pids.tsv"

  # listen ports · prefer 127 inside
  ss -ltnH 2>/dev/null | awk '{print $4}' | sort -u >"$dest/listen.txt" || : >"$dest/listen.txt"
  # thrash public only · Apache 80/443 on own IPs = desk HTTPS (not thrash)
  list_thrash_public | sort -u >"$dest/listen_public.txt" || : >"$dest/listen_public.txt"
  list_desk_public_ok | sort -u >"$dest/listen_desk_public.txt" || : >"$dest/listen_desk_public.txt"

  # system services running
  systemctl list-units --type=service --state=running --no-legend --no-pager 2>/dev/null \
    | awk '{print $1}' | sort -u >"$dest/services.txt" || : >"$dest/services.txt"
  systemctl --user list-units --type=service --state=running --no-legend --no-pager 2>/dev/null \
    | awk '{print $1}' | sort -u >"$dest/user_services.txt" || : >"$dest/user_services.txt"

  # grok / hostess / desk family
  ps -eo pid=,comm=,args= 2>/dev/null | awk '
    BEGIN{IGNORECASE=1}
    /grok|hostess|desk-rest|h7-|spv-|kate|bgrtx|claws|waterfox/ {
      pid=$1; comm=$2; $1=$2=""; sub(/^ +/,"");
      args=$0; if(length(args)>140) args=substr(args,1,140);
      gsub(/\t/," ",args);
      printf "%s\t%s\t%s\n", pid, comm, args
    }' >"$dest/grok_family.tsv" || : >"$dest/grok_family.tsv"

  # file mtimes · unique INSIDE roots · unique paths (no join balloon)
  # note: find|head can SIGPIPE (141) under pipefail — always || true
  local tmpf r seen_roots rr
  tmpf="$(mktemp)"
  seen_roots="$(mktemp)"
  : >"$tmpf"
  for r in "${INSIDE_ROOTS[@]}"; do
    [[ -d "$r" ]] || continue
    rr="$(realpath "$r" 2>/dev/null || echo "$r")"
    grep -Fxq "$rr" "$seen_roots" 2>/dev/null && continue
    echo "$rr" >>"$seen_roots"
    # -printf avoids per-file stat spawn · exclude noise paths in find
    find "$rr" -xdev -type f \
      \( -name '*.sh' -o -name '*.asm' -o -name '*.py' -o -name '*.json' -o -name '*.html' -o -name '*.js' -o -name '*.service' \) \
      ! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/__pycache__/*' \
      ! -path '*/out/h7fast/*' ! -path '*/out/h7secure/*' \
      ! -name 'h7_secure_heuristics.tsv' ! -name 'h7_forever.log' ! -name 'h7_vocab_feed.tsv' \
      -printf '%T@\t%s\t%p\n' 2>/dev/null \
      | head -n 6000 \
      | awk -F'\t' 'NF>=3 {
          mt=int($1+0); sz=$2; path=$3;
          for(i=4;i<=NF;i++) path=path "\t" $i;
          printf "%s\t%s\t%s\n", mt, sz, path
        }' >>"$tmpf" || true
  done
  # keep last row per path → unique path key
  if [[ -s "$tmpf" ]]; then
    LC_ALL=C sort -t$'\t' -k3,3 "$tmpf" \
      | awk -F'\t' 'NF>=3 { mt[$3]=$1; sz[$3]=$2 } END { for (p in mt) printf "%s\t%s\t%s\n", mt[p], sz[p], p }' \
      | LC_ALL=C sort -t$'\t' -k3,3 >"$dest/files.tsv" || : >"$dest/files.tsv"
  else
    : >"$dest/files.tsv"
  fi
  rm -f "$tmpf" "$seen_roots"

  printf '%s\n' "${INSIDE_ROOTS[@]}" >"$dest/inside_roots.txt"
  date -Iseconds >"$dest/captured_at"
}

# ── diff baseline → heuristics ──
diff_learn() {
  local base="${1:-$BASE}" now="${2:-$NOW}"
  [[ -f "$base/pids.tsv" ]] || { say "no baseline · run baseline first"; return 1; }
  capture "$now"
  : >"$HEUR_TSV"
  local n_new_pid=0 n_gone_pid=0 n_new_svc=0 n_pub=0 n_file_new=0 n_file_chg=0 n_grok=0
  local n_thrash_skip=0
  local HEUR_CAP=400

  # PID comm set · filter kernel thrash (kworker etc)
  cut -f3 "$base/pids.tsv" | LC_ALL=C sort -u >"$now/base_comms.txt"
  cut -f3 "$now/pids.tsv" | LC_ALL=C sort -u >"$now/now_comms.txt"
  while IFS= read -r c; do
    [[ -z "$c" ]] && continue
    if is_thrash_comm "$c"; then
      n_thrash_skip=$((n_thrash_skip + 1))
      continue
    fi
    n_new_pid=$((n_new_pid + 1))
    [[ $n_new_pid -le 40 ]] && printf '%s\tNEW_COMM\tOUT?\t%s\tnew process class\n' "$TS" "$c" >>"$HEUR_TSV"
  done < <(comm -13 "$now/base_comms.txt" "$now/now_comms.txt")
  while IFS= read -r c; do
    [[ -z "$c" ]] && continue
    is_thrash_comm "$c" && continue
    n_gone_pid=$((n_gone_pid + 1))
    [[ $n_gone_pid -le 40 ]] && printf '%s\tGONE_COMM\tINSIDE\t%s\tprocess class missing\n' "$TS" "$c" >>"$HEUR_TSV"
  done < <(comm -23 "$now/base_comms.txt" "$now/now_comms.txt")

  # services
  while IFS= read -r s; do
    [[ -z "$s" ]] && continue
    n_new_svc=$((n_new_svc + 1))
    local side=INSIDE
    if echo "$s" | grep -qiE 'snap|avahi|rpc|telnet|ftp|docker|containerd|cups-browsed'; then
      side=OUTSIDE
    fi
    printf '%s\tNEW_SERVICE\t%s\t%s\tsystemd running\n' "$TS" "$side" "$s" >>"$HEUR_TSV"
  done < <(comm -13 <(LC_ALL=C sort -u "$base/services.txt" 2>/dev/null || true) <(LC_ALL=C sort -u "$now/services.txt" 2>/dev/null || true))

  # public listen (OUTSIDE risk)
  local n_desk_pub=0
  if [[ -s "$now/listen_desk_public.txt" ]]; then
    n_desk_pub=$(n0 "$(wc -l <"$now/listen_desk_public.txt" | tr -d ' ')")
  fi
  if [[ -s "$now/listen_public.txt" ]]; then
    while IFS= read -r p; do
      [[ -z "$p" ]] && continue
      n_pub=$((n_pub + 1))
      printf '%s\tPUBLIC_LISTEN\tOUTSIDE\t%s\tnot 127 · keep outside out\n' "$TS" "$p" >>"$HEUR_TSV"
    done <"$now/listen_public.txt"
  fi

  # grok family · sample only (count full)
  n_grok=$(n0 "$(wc -l <"$now/grok_family.tsv" | tr -d ' ')")
  head -20 "$now/grok_family.tsv" 2>/dev/null | while IFS=$'\t' read -r pid comm args; do
    [[ -z "$pid" ]] && continue
    printf '%s\tGROK_FAMILY\tINSIDE\t%s:%s\t%s\n' "$TS" "$pid" "$comm" "${args:0:80}"
  done >>"$HEUR_TSV"

  # file changes · unique paths · awk (no cartesian join balloon)
  if [[ -s "$base/files.tsv" && -s "$now/files.tsv" ]]; then
    cut -f3 "$base/files.tsv" | LC_ALL=C sort -u >"$now/base_paths.txt"
    cut -f3 "$now/files.tsv" | LC_ALL=C sort -u >"$now/now_paths.txt"
    while IFS= read -r path; do
      [[ -z "$path" ]] && continue
      n_file_new=$((n_file_new + 1))
      if [[ $n_file_new -le 30 ]]; then
        local sz
        sz=$(awk -F'\t' -v p="$path" '$3==p {print $2; exit}' "$now/files.tsv")
        printf '%s\tFILE_NEW\tINSIDE\t%s\tsz=%s\n' "$TS" "$path" "${sz:-?}" >>"$HEUR_TSV"
      fi
    done < <(comm -13 "$now/base_paths.txt" "$now/now_paths.txt")

    local chg_tmp chg_n_file
    chg_tmp="$(mktemp)"
    chg_n_file="${chg_tmp}.n"
    awk -F'\t' -v ts="$TS" -v nfile="$chg_n_file" '
      FNR==NR { if (NF>=3) { bmt[$3]=$1; bsz[$3]=$2 } next }
      NF>=3 && ($3 in bmt) && (bmt[$3]!=$1 || bsz[$3]!=$2) {
        n++
        if (n<=60) printf "%s\tFILE_CHG\tINSIDE\t%s\told=%s/%s new=%s/%s\n", ts, $3, bmt[$3], bsz[$3], $1, $2
      }
      END { print n+0 > nfile }
    ' "$base/files.tsv" "$now/files.tsv" >"$chg_tmp"
    cat "$chg_tmp" >>"$HEUR_TSV"
    n_file_chg=$(n0 "$(cat "$chg_n_file" 2>/dev/null || echo 0)")
    rm -f "$chg_tmp" "$chg_n_file"
  fi

  # score · n0 never doubles (grep -c||echo 0 → "00")
  local outside_n inside_n outq
  outside_n=$(n0 "$(grep -c $'\tOUTSIDE\t' "$HEUR_TSV" 2>/dev/null || true)")
  inside_n=$(n0 "$(grep -c $'\tINSIDE\t' "$HEUR_TSV" 2>/dev/null || true)")
  outq=$(n0 "$(grep -c $'\tOUT?\t' "$HEUR_TSV" 2>/dev/null || true)")

  local thrash_deny=0
  if [[ -x "$ROOT/out/h7fast/bin/operate_when" ]]; then
    thrash_deny=$(n0 "$("$ROOT/out/h7fast/bin/operate_when" 3 2>/dev/null | tail -1 || true)")
  fi

  local ok=true
  [[ "$n_pub" -gt 0 || "$outside_n" -gt 5 ]] && ok=false

  if [[ $(n0 "$(wc -l <"$HEUR_TSV" | tr -d ' ')") -gt $HEUR_CAP ]]; then
    head -n "$HEUR_CAP" "$HEUR_TSV" >"${HEUR_TSV}.tmp" && mv "${HEUR_TSV}.tmp" "$HEUR_TSV"
  fi

  cat >"$LEARN_JSON" <<EOF
{
  "ts": "$TS",
  "word": "H7_SECURE_LEARN",
  "stack": "bash+asm",
  "free_ball": true,
  "ok": $ok,
  "inside_out": {
    "keep_inside_in": true,
    "keep_outside_out": true,
    "public_listen_n": $n_pub,
    "desk_public_n": $n_desk_pub,
    "desk_https_ok": true,
    "outside_heuristics": $outside_n,
    "inside_heuristics": $inside_n,
    "soft_out_q": $outq
  },
  "learned": {
    "new_comm_classes": $n_new_pid,
    "gone_comm_classes": $n_gone_pid,
    "new_services": $n_new_svc,
    "file_new": $n_file_new,
    "file_chg": $n_file_chg,
    "grok_family_pids": $n_grok,
    "thrash_comm_skipped": $n_thrash_skip
  },
  "gates": {
    "operate_when_thrash_deny": "$thrash_deny",
    "want_deny": "0"
  },
  "paths": {
    "baseline": "out/h7secure/baseline",
    "now": "out/h7secure/now",
    "heuristics": "out/h7_secure_heuristics.tsv"
  },
  "law": "learn PIDs services daemons grok file-changes as heuristics · love whole internet as field · clear dirt · INSIDE in · OUTSIDE out · free-ball · never ML"
}
EOF

  {
    printf '%s\t%s\t%s\t%s\n' "$TS" "secure_learn" "SECURITY" "learn"
    grep $'\tOUTSIDE\t' "$HEUR_TSV" 2>/dev/null | head -8 | awk -F'\t' -v ts="$TS" '{print ts"\t"$4"\tSECURITY\talert"}'
    grep $'\tGROK_FAMILY\t' "$HEUR_TSV" 2>/dev/null | head -5 | awk -F'\t' -v ts="$TS" '{print ts"\tgrok_"$4"\tSECURITY\tscroll"}'
    grep $'\tNEW_COMM\t' "$HEUR_TSV" 2>/dev/null | head -5 | awk -F'\t' -v ts="$TS" '{print ts"\t"$4"\tSECURITY\tnew"}'
  } >>"$FEED" 2>/dev/null || true

  say "SECURE LEARN · new_comm=$n_new_pid thrash_skip=$n_thrash_skip new_svc=$n_new_svc pub=$n_pub files_new=$n_file_new files_chg=$n_file_chg grok=$n_grok outside=$outside_n ok=$ok"
  cat "$LEARN_JSON"
}

baseline() {
  say "BASELINE · capture INSIDE world"
  capture "$BASE"
  cp -a "$BASE/." "$NOW/" 2>/dev/null || true
  say "baseline pids=$(wc -l <"$BASE/pids.tsv") services=$(wc -l <"$BASE/services.txt") files=$(wc -l <"$BASE/files.tsv") grok=$(wc -l <"$BASE/grok_family.tsv")"
  # first learn = empty diff ok
  : >"$HEUR_TSV"
  cat >"$LEARN_JSON" <<EOF
{
  "ts": "$TS",
  "word": "H7_SECURE_BASELINE",
  "ok": true,
  "pids": $(wc -l <"$BASE/pids.tsv" | tr -d ' '),
  "services": $(wc -l <"$BASE/services.txt" | tr -d ' '),
  "files": $(wc -l <"$BASE/files.tsv" | tr -d ' '),
  "grok_family": $(wc -l <"$BASE/grok_family.tsv" | tr -d ' '),
  "law": "baseline learned · INSIDE inventory · free-ball"
}
EOF
  cat "$LEARN_JSON"
}

status() {
  if [[ -f "$LEARN_JSON" ]]; then cat "$LEARN_JSON"; else say "no learn yet · run baseline then once"; fi
  echo "--- heuristics (last 15) ---"
  tail -15 "$HEUR_TSV" 2>/dev/null || true
  echo "--- grok now ---"
  head -10 "$NOW/grok_family.tsv" 2>/dev/null || true
  echo "--- public listen ---"
  cat "$NOW/listen_public.txt" 2>/dev/null || echo "(none)"
}

case "$cmd" in
  baseline) baseline ;;
  learn|diff|once)
    if [[ ! -f "$BASE/pids.tsv" ]]; then baseline; fi
    diff_learn
    ;;
  status) status ;;
  capture) capture "$NOW"; say "captured now" ;;
  *)
    echo "usage: $0 baseline|once|learn|status" >&2
    echo "  learn PIDs services daemons grok file-changes · INSIDE in · OUTSIDE out" >&2
    exit 2
    ;;
esac
