# nomarchy-doctor — one-shot, READ-ONLY health check: the things that
# actually break user machines, as a pass/fail sheet where every
# failure prints the one command that fixes it. It never changes
# anything itself. Exit 0 = no failures (warnings allowed), 1 = at
# least one ✖.

flake="${NOMARCHY_PATH:-$HOME/.nomarchy}"
fails=0
warns=0

grn=$'\033[32m'; red=$'\033[31m'; ylw=$'\033[33m'; dim=$'\033[2m'; rst=$'\033[0m'
ok()   { printf '  %b✔%b %s\n' "$grn" "$rst" "$1"; }
bad()  { printf '  %b✖%b %s\n' "$red" "$rst" "$1"
         printf '      %bfix: %s%b\n' "$dim" "$2" "$rst"
         fails=$((fails + 1)); }
warn() { printf '  %b●%b %s\n' "$ylw" "$rst" "$1"
         if [ $# -gt 1 ]; then printf '      %b%s%b\n' "$dim" "$2" "$rst"; fi
         warns=$((warns + 1)); }
skip() { printf '  %b– %s%b\n' "$dim" "$1" "$rst"; }

printf 'nomarchy-doctor — %s\n\n' "$(date '+%Y-%m-%d %H:%M')"

# ── systemd units ────────────────────────────────────────────────────
mapfile -t sysfailed < <(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [ "${#sysfailed[@]}" -eq 0 ]; then
  ok "no failed system units"
else
  bad "failed system unit(s): ${sysfailed[*]}" \
      "journalctl -b -u <unit>  (read why; sys-rebuild after fixing)"
fi

# A user bus only exists inside a session (not over bare SSH/root).
if [ -n "$(systemctl --user is-system-running 2>/dev/null || true)" ]; then
  mapfile -t usrfailed < <(systemctl --user --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
  if [ "${#usrfailed[@]}" -eq 0 ]; then
    ok "no failed user units"
  else
    bad "failed user unit(s): ${usrfailed[*]}" \
        "journalctl --user -b -u <unit>"
  fi
else
  skip "user units (no user session bus here)"
fi

# ── disk space ───────────────────────────────────────────────────────
# Only real on-disk filesystems; skips the tmpfs/9p/overlay mounts of
# live systems and test VMs (where usage numbers mean nothing).
seen=""
for mp in / /boot /nix; do
  [ -d "$mp" ] || continue
  line=$(df -PT "$mp" 2>/dev/null | awk 'NR==2 {gsub("%","",$6); print $1, $2, $6, $5}')
  [ -n "$line" ] || continue
  read -r dev fstype use availkb <<<"$line"
  case "$fstype" in
    ext2|ext3|ext4|btrfs|xfs|vfat|f2fs|zfs) ;;
    *) continue ;;
  esac
  case " $seen " in *" $dev "*) continue ;; esac
  seen="$seen $dev"
  avail=$(numfmt --to=iec $((availkb * 1024)))
  if [ "$use" -ge 90 ]; then
    bad "$mp is ${use}% full (${avail} free)" \
        "sudo nix-collect-garbage --delete-older-than 14d  (then sys-rebuild to prune old boot entries)"
  else
    ok "$mp has space (${use}% used, ${avail} free)"
  fi
done

# ── the flake + state file ───────────────────────────────────────────
if [ -f "$flake/theme-state.json" ]; then
  if jq empty "$flake/theme-state.json" 2>/dev/null; then
    ok "theme-state.json parses"
  else
    bad "theme-state.json is not valid JSON (rebuilds will fail)" \
        "nomarchy-theme-sync validate  (names the spot; fix it, or re-apply a theme)"
  fi
  if git -C "$flake" ls-files --error-unmatch theme-state.json >/dev/null 2>&1; then
    ok "theme-state.json is git-tracked"
  else
    bad "theme-state.json is NOT git-tracked (flake evaluation can't see it)" \
        "git -C $flake add theme-state.json"
  fi
else
  skip "theme-state.json (no flake checkout at $flake)"
fi

if [ -d "$flake/.git" ]; then
  dirty=$(git -C "$flake" status --porcelain 2>/dev/null | wc -l)
  if [ "$dirty" -gt 0 ]; then
    warn "$dirty uncommitted change(s) in $flake (normal — theme writes land there)" \
         "commit when happy: git -C $flake add -A && git -C $flake commit -m settings"
  else
    ok "flake checkout is clean"
  fi
  behind=$(git -C "$flake" rev-list --count 'HEAD..@{u}' 2>/dev/null || echo 0)
  if [ "$behind" -gt 0 ]; then
    warn "flake is $behind commit(s) behind its upstream" "git -C $flake pull, then sys-rebuild + home-update"
  fi
fi

# ── generation age ───────────────────────────────────────────────────
# The profile SYMLINK's own mtime is the generation's creation time
# (store paths themselves are all epoch-1).
link=$(readlink /nix/var/nix/profiles/system 2>/dev/null || true)
if [ -n "$link" ] && [ -e "/nix/var/nix/profiles/$link" ]; then
  ts=$(stat -c %Y "/nix/var/nix/profiles/$link" 2>/dev/null || echo 0)
  days=$(( ($(date +%s) - ts) / 86400 ))
  if [ "$days" -gt 30 ]; then
    warn "last system rebuild was $days days ago" \
         "sys-update when convenient (security fixes arrive with input bumps)"
  else
    ok "system generation is $days day(s) old"
  fi
else
  skip "system generation age (no system profile)"
fi

# ── snapper timeline (when enabled) ──────────────────────────────────
if [ -n "$(systemctl list-unit-files snapper-timeline.timer --no-legend --plain 2>/dev/null)" ]; then
  if systemctl is-active --quiet snapper-timeline.timer; then
    ok "snapper timeline snapshots are running"
  else
    bad "snapper is enabled but the timeline timer is not active" \
        "systemctl status snapper-timeline.timer  (then journalctl -u snapper-timeline.service)"
  fi
else
  skip "snapper (not enabled on this machine)"
fi

# ══ hardware ═════════════════════════════════════════════════════════
# Every check below self-gates: it skips cleanly when the tool, service,
# or device isn't present, so the section shrinks to fit the machine and
# never fails just because a feature is absent. Still read-only.

# ── network (NetworkManager) ─────────────────────────────────────────
if command -v nmcli >/dev/null 2>&1; then
  nmstate=$(nmcli -t -f STATE general status 2>/dev/null || true)
  case "$nmstate" in
    connected*) ok "NetworkManager: $nmstate" ;;
    "")         skip "NetworkManager (no state reported)" ;;
    *)          warn "NetworkManager state: $nmstate" \
                     "connect in System › Network (or: nmcli device)" ;;
  esac
else
  skip "NetworkManager (nmcli not present)"
fi

# ── default audio sink (PipeWire) ────────────────────────────────────
# wpctl/pactl talk to the user's PipeWire session — only meaningful with
# a user bus (not over bare SSH/root).
if [ -n "$(systemctl --user is-system-running 2>/dev/null || true)" ]; then
  if command -v wpctl >/dev/null 2>&1; then
    if wpctl inspect @DEFAULT_AUDIO_SINK@ >/dev/null 2>&1; then
      ok "default audio sink present"
    else
      warn "no default PipeWire sink" "pick one in System › Audio (or: wpctl status)"
    fi
  elif command -v pactl >/dev/null 2>&1; then
    defsink=$(pactl get-default-sink 2>/dev/null || true)
    if [ -n "$defsink" ] && [ "$defsink" != "@DEFAULT_SINK@" ]; then
      ok "default audio sink: $defsink"
    else
      warn "no default audio sink" "pick one in System › Audio"
    fi
  else
    skip "audio sink (no wpctl/pactl)"
  fi
else
  skip "audio sink (no user session bus here)"
fi

# ── GPU acceleration smoke (only if the probes are installed) ────────
if command -v vainfo >/dev/null 2>&1; then
  if vainfo >/dev/null 2>&1; then
    ok "VA-API acceleration works (vainfo)"
  else
    warn "vainfo present but VA-API probe failed" \
         "check mkFlake.hardwareProfile / GPU drivers (or run: vainfo)"
  fi
else
  skip "VA-API smoke (vainfo not installed)"
fi
if command -v glxinfo >/dev/null 2>&1; then
  if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]; then
    if glxinfo -B >/dev/null 2>&1; then
      ok "OpenGL renderer responds (glxinfo)"
    else
      warn "glxinfo present but GL probe failed" \
           "check mkFlake.hardwareProfile / GPU drivers (or run: glxinfo -B)"
    fi
  else
    skip "OpenGL smoke (no display attached)"
  fi
else
  skip "OpenGL smoke (glxinfo not installed)"
fi

# ── fingerprint (only when fprintd is enabled) ───────────────────────
if [ -n "$(systemctl list-unit-files fprintd.service --no-legend --plain 2>/dev/null || true)" ]; then
  enrolled=""
  if command -v fprintd-list >/dev/null 2>&1 && [ -n "${USER:-}" ]; then
    enrolled=$(fprintd-list "$USER" 2>/dev/null || true)
  fi
  if [ -n "$enrolled" ] && ! printf '%s' "$enrolled" | grep -qi 'no fingers'; then
    ok "fprintd enabled, fingerprint(s) enrolled for ${USER:-user}"
  elif [ -n "$enrolled" ]; then
    warn "fprintd enabled but no fingerprints enrolled" "enroll one: fprintd-enroll"
  else
    ok "fprintd unit installed (fingerprint enabled)"
  fi
else
  skip "fingerprint (fprintd not enabled)"
fi

# ── firmware updates (fwupd) ─────────────────────────────────────────
# fwupd is D-Bus-activated, so an idle daemon is normal, not a fault.
# get-updates reads local metadata (no flashing); a hit is a soft warn.
if command -v fwupdmgr >/dev/null 2>&1 \
   && [ -n "$(systemctl list-unit-files fwupd.service --no-legend --plain 2>/dev/null || true)" ]; then
  if systemctl is-active --quiet fwupd.service; then
    ok "fwupd daemon active"
  else
    skip "fwupd daemon (idle — D-Bus-activated on demand)"
  fi
  if timeout 20 fwupdmgr get-updates >/dev/null 2>&1; then
    warn "firmware updates are available" "review, then apply: fwupdmgr update"
  else
    ok "firmware up to date (no pending updates)"
  fi
else
  skip "fwupd (not enabled / fwupdmgr absent)"
fi

# ── laptop battery charge threshold (only when a limit is set) ───────
# Name-agnostic (BAT0, CMB0, …) — same type/scope filter as notify (#60).
bat_seen=0; bat_limited=0
for bat in /sys/class/power_supply/*/; do
  [ "$(cat "$bat/type" 2>/dev/null)" = Battery ] || continue
  [ "$(cat "$bat/scope" 2>/dev/null || echo System)" = Device ] && continue
  f="$bat/charge_control_end_threshold"
  [ -r "$f" ] || continue
  bat_seen=1
  lim=$(cat "$f" 2>/dev/null || true)
  case "$lim" in
    ''|*[!0-9]*) continue ;;
  esac
  if [ "$lim" -lt 100 ]; then
    bat_limited=1
    ok "$(basename "$bat") charge limit active at ${lim}%"
  fi
done
if [ "$bat_seen" -eq 1 ] && [ "$bat_limited" -eq 0 ]; then
  skip "battery charge limit (none set — charges to 100%)"
fi

# ── verdict ──────────────────────────────────────────────────────────
echo
if [ "$fails" -eq 0 ]; then
  printf '%b✔ healthy%b — %d warning(s)\n' "$grn" "$rst" "$warns"
  exit 0
else
  printf '%b✖ %d problem(s)%b, %d warning(s) — each ✖ above shows its fix\n' "$red" "$fails" "$rst" "$warns"
  exit 1
fi
