#!/usr/bin/env bash
# blip-hw-report.sh
# Linux counterpart of blip-hw-report.ps1. Reports this machine's hardware specs
# to the blip-hw endpoint. Run once to register; re-run anytime to update.
#
# Usage:
#   bash blip-hw-report.sh
#   bash blip-hw-report.sh --label "m920qLinux"
#   bash blip-hw-report.sh --label "rpi5" --endpoint "https://my.jetblip.com/blip-hw/report"
#
# Notes:
#   - RAM sticks + motherboard need dmidecode (root). The script tries `sudo -n`;
#     if not permitted it still reports CPU/RAM total/disks/GPU and leaves those empty.
#   - Needs: lscpu, free, lsblk, lspci (pciutils), python3 (stdlib only). dmidecode optional.

set -u
LABEL=""
ENDPOINT="https://my.jetblip.com/blip-hw/report"
while [ $# -gt 0 ]; do
  case "$1" in
    --label)    LABEL="$2"; shift 2 ;;
    --endpoint) ENDPOINT="$2"; shift 2 ;;
    *) echo "unknown arg: $1" >&2; exit 2 ;;
  esac
done

HOST="$(hostname | tr '[:upper:]' '[:lower:]')"
[ -z "$LABEL" ] && LABEL="$HOST"

# sudo helper (non-interactive only; degrade gracefully)
SUDO=""
if command -v dmidecode >/dev/null 2>&1; then
  if [ "$(id -u)" = "0" ]; then SUDO=""
  elif sudo -n true 2>/dev/null; then SUDO="sudo -n"; fi
fi

# --- collect into a TSV stream that python parses ---
collect() {
  # OS
  OS="$( . /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-$(uname -sr)}" )"
  printf 'K\tos\t%s\n' "$OS"

  # CPU (lscpu)
  if command -v lscpu >/dev/null 2>&1; then
    LC_ALL=C lscpu > /tmp/.bhw_lscpu 2>/dev/null
    name="$(awk -F: '/^Model name/{sub(/^[ \t]+/,"",$2);print $2;exit}' /tmp/.bhw_lscpu)"
    cps="$(awk -F: '/^Core\(s\) per socket/{gsub(/ /,"",$2);print $2;exit}' /tmp/.bhw_lscpu)"
    sock="$(awk -F: '/^Socket\(s\)/{gsub(/ /,"",$2);print $2;exit}' /tmp/.bhw_lscpu)"
    threads="$(nproc --all 2>/dev/null)"
    mhz="$(awk -F: '/^CPU max MHz/{gsub(/ /,"",$2);printf "%d",$2;exit}' /tmp/.bhw_lscpu)"
    [ -z "$mhz" ] && mhz="$(awk -F: '/^CPU MHz/{gsub(/ /,"",$2);printf "%d",$2;exit}' /tmp/.bhw_lscpu)"
    cores=$(( ${cps:-0} * ${sock:-1} )); [ "$cores" = "0" ] && cores=""
    printf 'K\tcpu_name\t%s\n' "$name"
    printf 'K\tcpu_cores\t%s\n' "$cores"
    printf 'K\tcpu_threads\t%s\n' "$threads"
    printf 'K\tcpu_mhz\t%s\n' "$mhz"
  fi

  # RAM total (GiB, matches the .ps1 1GB=2^30 convention)
  memkb="$(awk '/^MemTotal:/{print $2}' /proc/meminfo)"
  printf 'K\tram_gb\t%s\n' "$(awk -v k="$memkb" 'BEGIN{printf "%.1f", k/1048576}')"

  # RAM sticks (dmidecode, needs root)
  if command -v dmidecode >/dev/null 2>&1 && { [ "$(id -u)" = 0 ] || [ -n "$SUDO" ]; }; then
    $SUDO dmidecode -t memory 2>/dev/null | awk '
      /Memory Device/{indev=1; cap="";spd="";man="";part="";bank="";loc=""}
      indev&&/^\tSize:/{v=$0;sub(/^\tSize:[ ]*/,"",v); if(v ~ /No Module|Unknown/){cap="SKIP"} else {n=v; sub(/ .*/,"",n); cap=n}}
      indev&&/^\tLocator:/&&!/Bank/{v=$0;sub(/^\tLocator:[ ]*/,"",v);loc=v}
      indev&&/Bank Locator:/{v=$0;sub(/.*Bank Locator:[ ]*/,"",v);bank=v}
      indev&&/^\tSpeed:/{v=$0;sub(/^\tSpeed:[ ]*/,"",v);n=v;sub(/ .*/,"",n);spd=n}
      indev&&/Manufacturer:/{v=$0;sub(/.*Manufacturer:[ ]*/,"",v);man=v}
      indev&&/Part Number:/{v=$0;sub(/.*Part Number:[ ]*/,"",v);part=v}
      indev&&/^$/{ if(cap!=""&&cap!="SKIP"){printf "RAM\t%s\t%s\t%s\t%s\t%s\t%s\n",cap,spd,man,part,bank,loc} indev=0 }
    '
    # motherboard
    mbman="$($SUDO dmidecode -t baseboard 2>/dev/null | awk -F: '/Manufacturer:/{sub(/^[ \t]+/,"",$2);print $2;exit}')"
    mbpro="$($SUDO dmidecode -t baseboard 2>/dev/null | awk -F: '/Product Name:/{sub(/^[ \t]+/,"",$2);print $2;exit}')"
    printf 'K\tmobo_manufacturer\t%s\n' "$mbman"
    printf 'K\tmobo_product\t%s\n' "$mbpro"
  fi

  # Disks (physical, no partitions)
  if command -v lsblk >/dev/null 2>&1; then
    lsblk -dbno NAME,MODEL,SIZE,TRAN,SERIAL,ROTA 2>/dev/null | while read -r name model size tran serial rota; do
      case "$name" in loop*|ram*|sr*) continue;; esac
      sgb="$(awk -v s="${size:-0}" 'BEGIN{printf "%d", s/1073741824}')"
      media="SSD"; [ "${rota:-0}" = "1" ] && media="HDD"
      printf 'DISK\t%s\t%s\t%s\t%s\t%s\n' "${model:-}" "$sgb" "${tran:-}" "${serial:-}" "$media"
    done
  fi

  # GPU
  if command -v lspci >/dev/null 2>&1; then
    lspci -mm 2>/dev/null | grep -Ei 'vga compatible|3d controller|display controller' | while IFS= read -r line; do
      gname="$(printf '%s' "$line" | sed -E 's/^[0-9a-f:.]+ "[^"]*" "([^"]*)" "([^"]*)".*/\1 \2/')"
      printf 'GPU\t%s\t%s\n' "$gname" ""
    done
  fi
}

# --- assemble JSON + POST (python3 stdlib) ---
# Collect into a temp file (python reads the file; its own program arrives on stdin via heredoc).
DATAFILE="$(mktemp)"; trap 'rm -f "$DATAFILE"' EXIT
collect > "$DATAFILE"
LABEL="$LABEL" HOST="$HOST" ENDPOINT="$ENDPOINT" DATAFILE="$DATAFILE" python3 <<'PY'
import os, sys, json, urllib.request
label=os.environ["LABEL"]; host=os.environ["HOST"]; endpoint=os.environ["ENDPOINT"]
p={"hostname":host,"label":label,"os":None,"cpu_name":None,"cpu_cores":None,
   "cpu_threads":None,"cpu_mhz":None,"ram_gb":None,"ram":[],"gpu":[],"disks":[],
   "mobo_manufacturer":None,"mobo_product":None}
def num(x):
    try: return int(x)
    except:
        try: return float(x)
        except: return None
for raw in open(os.environ["DATAFILE"], encoding="utf-8", errors="replace"):
    f=raw.rstrip("\n").split("\t")
    if not f or not f[0]: continue
    if f[0]=="K":
        k=f[1]; v=f[2] if len(f)>2 else ""
        if k in ("cpu_cores","cpu_threads","cpu_mhz"): p[k]=num(v)
        elif k=="ram_gb": p[k]=num(v)
        else: p[k]=v or None
    elif f[0]=="RAM":
        p["ram"].append({"capacity_gb":num(f[1]),"speed_mhz":num(f[2]),
            "manufacturer":(f[3] or None),"part_number":(f[4] or None),
            "bank":(f[5] or None),"slot":(f[6] if len(f)>6 else None) or None})
    elif f[0]=="DISK":
        p["disks"].append({"model":(f[1] or None),"size_gb":num(f[2]),
            "interface":(f[3] or None),"serial":(f[4] or None),
            "media_type":(f[5] if len(f)>5 else None)})
    elif f[0]=="GPU":
        p["gpu"].append({"name":(f[1] or None),"vram_bytes":None,
            "driver":(f[2] if len(f)>2 else None) or None})
body=json.dumps(p).encode()
print(f"Reporting: {label} ({host})")
print(f"  CPU:  {p['cpu_name']}")
print(f"  RAM:  {p['ram_gb']} GB  ({len(p['ram'])} sticks)")
print(f"  Disks:{', '.join((d['model'] or '?') for d in p['disks']) or '-'}")
print(f"  Endpoint: {endpoint}")
try:
    req=urllib.request.Request(endpoint,data=body,method="POST",headers={
        "Content-Type":"application/json",
        "User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36 blip-hw"})
    with urllib.request.urlopen(req,timeout=20) as r:
        print(f"Done [{r.status}]: {r.read().decode()[:300]}")
except Exception as e:
    print(f"ERROR: {e}"); sys.exit(1)
PY
