#!/usr/bin/bash
# veilor-doctor — read-only diagnostic / health check.
# User-facing CLI shipped in /usr/local/bin/. v0.6 ergonomic tooling.
#
# Reports on system, hardening, disk, network, updates, veilor units.
# No fixes are ever applied — output only. Use this to verify drift
# from the v0.2+ baseline.
#
# Flags:
#   --quiet  print only PASS/FAIL summary
#   --json   emit JSON for monitoring
#   -h|--help

set -uo pipefail

QUIET=0
JSON=0
for arg in "$@"; do
    case "$arg" in
        --quiet|-q)  QUIET=1 ;;
        --json)      JSON=1 ;;
        -h|--help)
            sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
            exit 0
            ;;
        *)
            echo "unknown flag: $arg" >&2
            exit 2
            ;;
    esac
done

have() { command -v "$1" >/dev/null 2>&1; }

# ── Output helpers ──────────────────────────────────────────────────
PASS=0
FAIL=0
ROWS=()       # human table rows: "Section|Check|Status|Detail"
JSON_ROWS=()  # JSON-serialisable rows

# Use color only if stdout is a TTY and we're not in --quiet/--json mode.
if [[ -t 1 && $QUIET -eq 0 && $JSON -eq 0 ]]; then
    GREEN=$'\033[32m'; RED=$'\033[31m'; DIM=$'\033[2m'; OFF=$'\033[0m'
else
    GREEN=""; RED=""; DIM=""; OFF=""
fi

# JSON-escape a string for embedding.
json_esc() {
    local s=$1
    s=${s//\\/\\\\}
    s=${s//\"/\\\"}
    s=${s//$'\n'/\\n}
    s=${s//$'\t'/\\t}
    printf '%s' "$s"
}

# check <section> <name> <pass|fail> <detail>
check() {
    local section=$1 name=$2 status=$3 detail=$4
    if [[ $status == pass ]]; then
        PASS=$((PASS+1))
    else
        FAIL=$((FAIL+1))
    fi
    ROWS+=("${section}|${name}|${status}|${detail}")
    JSON_ROWS+=("{\"section\":\"$(json_esc "$section")\",\"name\":\"$(json_esc "$name")\",\"status\":\"$status\",\"detail\":\"$(json_esc "$detail")\"}")
}

# ── 1. System ───────────────────────────────────────────────────────
HOSTNAME_VAL=$(hostnamectl --static 2>/dev/null || hostname)
OS_NAME=$(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-unknown}")
KERNEL=$(uname -r)
UPTIME=$(uptime -p 2>/dev/null || uptime)
check System hostname pass "$HOSTNAME_VAL"
check System os pass "$OS_NAME"
check System kernel pass "$KERNEL"
check System uptime pass "$UPTIME"

# ── 2. Hardening ────────────────────────────────────────────────────
SELINUX=$(getenforce 2>/dev/null || echo "unknown")
[[ $SELINUX == "Enforcing" ]] && check Hardening selinux pass "$SELINUX" \
    || check Hardening selinux fail "$SELINUX (expected Enforcing)"

if systemctl is-active --quiet usbguard; then
    check Hardening usbguard pass active
else
    check Hardening usbguard fail "$(systemctl is-active usbguard 2>/dev/null || echo missing)"
fi

if systemctl is-active --quiet fail2ban; then
    check Hardening fail2ban pass active
else
    check Hardening fail2ban fail "$(systemctl is-active fail2ban 2>/dev/null || echo missing)"
fi

FW_ZONE=$(firewall-cmd --get-default-zone 2>/dev/null || echo unknown)
[[ $FW_ZONE == "drop" ]] && check Hardening firewalld_zone pass "$FW_ZONE" \
    || check Hardening firewalld_zone fail "$FW_ZONE (expected drop)"

PTRACE=$(sysctl -n kernel.yama.ptrace_scope 2>/dev/null || echo "")
[[ ${PTRACE:-0} -ge 2 ]] && check Hardening ptrace_scope pass "$PTRACE" \
    || check Hardening ptrace_scope fail "${PTRACE:-unset} (expected >=2)"

KPTR=$(sysctl -n kernel.kptr_restrict 2>/dev/null || echo "")
[[ ${KPTR:-0} -ge 2 ]] && check Hardening kptr_restrict pass "$KPTR" \
    || check Hardening kptr_restrict fail "${KPTR:-unset} (expected >=2)"

# ── 3. Disk ─────────────────────────────────────────────────────────
LUKS_DEV=$(lsblk -lno NAME,TYPE 2>/dev/null | awk '$2=="crypt" {print $1; exit}')
if [[ -n $LUKS_DEV ]]; then
    LUKS_STATUS=$(cryptsetup status "$LUKS_DEV" 2>/dev/null \
        | awk -F: '/cipher/ {gsub(/^ +/,"",$2); print $2; exit}')
    check Disk luks pass "${LUKS_DEV}: ${LUKS_STATUS:-active}"
else
    check Disk luks fail "no LUKS device found (full-disk encryption expected)"
fi

if have btrfs && btrfs filesystem df / >/dev/null 2>&1; then
    SUBVOLS=$(btrfs subvolume list / 2>/dev/null | wc -l)
    check Disk btrfs pass "${SUBVOLS} subvolume(s)"
else
    check Disk btrfs fail "btrfs not detected on /"
fi

ROOT_FREE=$(df -h / 2>/dev/null | awk 'NR==2 {print $4 " free / " $2 " (" $5 " used)"}')
check Disk root_free pass "${ROOT_FREE:-unknown}"

# ── 4. Network ──────────────────────────────────────────────────────
if systemctl is-active --quiet NetworkManager; then
    check Network networkmanager pass active
else
    check Network networkmanager fail inactive
fi

DEFAULT_ROUTE=$(ip -o route show default 2>/dev/null | awk '{print $3 " via " $5; exit}')
[[ -n $DEFAULT_ROUTE ]] && check Network default_route pass "$DEFAULT_ROUTE" \
    || check Network default_route fail "no default route"

DNS_LIST=$(awk '/^nameserver/ {print $2}' /etc/resolv.conf 2>/dev/null \
    | paste -sd, - 2>/dev/null)
[[ -n $DNS_LIST ]] && check Network dns pass "$DNS_LIST" \
    || check Network dns fail "no nameservers"

PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me 2>/dev/null || echo "")
[[ -n $PUBLIC_IP ]] && check Network public_ip pass "$PUBLIC_IP" \
    || check Network public_ip fail "lookup timed out"

# ── 5. Updates ──────────────────────────────────────────────────────
LAST_DNF=$(sudo -n dnf history list 2>/dev/null \
    | awk 'NR==4 {for(i=4;i<NF;i++)printf "%s ", $i; print $NF; exit}')
[[ -n $LAST_DNF ]] && check Updates last_dnf pass "$LAST_DNF" \
    || check Updates last_dnf pass "(unknown — try \`sudo dnf history\`)"

# `dnf check-update` exits 100 if updates available, 0 if not.
sudo -n dnf check-update -q >/dev/null 2>&1
RC=$?
case $RC in
    0)   check Updates pending pass "system up-to-date" ;;
    100)
        AVAIL=$(sudo -n dnf check-update -q 2>/dev/null \
            | awk 'NF>=3 && $1!~/^Last/ {n++} END {print n+0}')
        check Updates pending fail "${AVAIL} update(s) available — run \`veilor-update\`"
        ;;
    *)   check Updates pending fail "dnf check-update returned $RC (need sudo?)" ;;
esac

# ── 6. veilor services ──────────────────────────────────────────────
for unit in veilor-firstboot.service veilor-modules-lock.service; do
    if systemctl list-unit-files "$unit" 2>/dev/null | grep -q "$unit"; then
        STATE=$(systemctl is-enabled "$unit" 2>/dev/null || echo unknown)
        ACTIVE=$(systemctl is-active "$unit" 2>/dev/null || echo unknown)
        # firstboot is meant to be one-shot + disabled after run.
        check veilor "$unit" pass "${STATE} (${ACTIVE})"
    else
        check veilor "$unit" fail "unit not installed"
    fi
done

# ── Output ──────────────────────────────────────────────────────────
if [[ $JSON -eq 1 ]]; then
    printf '{"pass":%d,"fail":%d,"checks":[' "$PASS" "$FAIL"
    for i in "${!JSON_ROWS[@]}"; do
        [[ $i -gt 0 ]] && printf ','
        printf '%s' "${JSON_ROWS[$i]}"
    done
    printf ']}\n'
    [[ $FAIL -eq 0 ]] && exit 0 || exit 1
fi

if [[ $QUIET -eq 1 ]]; then
    if [[ $FAIL -eq 0 ]]; then
        echo "PASS ($PASS checks)"
        exit 0
    else
        echo "FAIL ($FAIL of $((PASS+FAIL)) checks failed)"
        exit 1
    fi
fi

_print_plain_table() {
    local last_section=""
    for r in "${ROWS[@]}"; do
        IFS='|' read -r sec name status detail <<<"$r"
        if [[ $sec != "$last_section" ]]; then
            printf '\n%s%s%s\n' "$DIM" "── $sec ──" "$OFF"
            last_section=$sec
        fi
        if [[ $status == pass ]]; then
            printf '  %s[OK]%s   %-20s %s\n' "$GREEN" "$OFF" "$name" "$detail"
        else
            printf '  %s[FAIL]%s %-20s %s\n' "$RED" "$OFF" "$name" "$detail"
        fi
    done
}

# Pretty table — gum if available, else plain. gum table reads tab-separated
# input on stdin; we feed it the rows then fall back to the plain printer
# if gum exits non-zero (e.g. no TTY).
if have gum; then
    {
        printf 'Section\tCheck\tStatus\tDetail\n'
        for r in "${ROWS[@]}"; do
            IFS='|' read -r sec name status detail <<<"$r"
            mark=$([[ $status == pass ]] && echo "OK" || echo "FAIL")
            printf '%s\t%s\t%s\t%s\n' "$sec" "$name" "$mark" "$detail"
        done
    } | gum table --print --separator $'\t' 2>/dev/null || _print_plain_table
else
    _print_plain_table
fi

echo
if [[ $FAIL -eq 0 ]]; then
    printf '%s%d checks passed.%s\n' "$GREEN" "$PASS" "$OFF"
    exit 0
else
    printf '%s%d of %d checks failed.%s\n' "$RED" "$FAIL" "$((PASS+FAIL))" "$OFF"
    exit 1
fi
