#!/usr/bin/env bash
# incusos-health -- Inspect IncusOS node health via the /os/1.0 REST API
#
# Queries one or more IncusOS cluster nodes for status, storage, security,
# services, network, and update information.
#
# Usage: incusos-health [OPTIONS] [ACTION]
# Run 'incusos-health --help' for full usage information.

set -euo pipefail

readonly VERSION="0.1.0"
readonly SCRIPT_NAME="incusos-health"

# ---------------------------------------------------------------------------
# Defaults
# ---------------------------------------------------------------------------

REMOTE="oc-node-01"
NODE=""
JSON_OUTPUT=false
DRY_RUN=false
VERBOSE=false
QUIET=false
ACTION=""

# Services to check (IncusOS built-in services)
readonly SERVICES=(iscsi lvm multipath nvme ovn tailscale usbip)

# ---------------------------------------------------------------------------
# Color and formatting
# ---------------------------------------------------------------------------

setup_colors() {
    if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        CYAN='\033[0;36m'
        BOLD='\033[1m'
        DIM='\033[2m'
        RESET='\033[0m'
    else
        RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
    fi
}

info()    { [[ "$QUIET" == true ]] && return; echo -e "${BLUE}[info]${RESET}    $*"; }
success() { [[ "$QUIET" == true ]] && return; echo -e "${GREEN}[ok]${RESET}      $*"; }
warn()    { echo -e "${YELLOW}[warn]${RESET}    $*" >&2; }
error()   { echo -e "${RED}[error]${RESET}   $*" >&2; }
step()    { [[ "$QUIET" == true ]] && return; echo -e "${CYAN}[step]${RESET}    ${BOLD}$*${RESET}"; }
detail()  { [[ "$VERBOSE" != true ]] && return; echo -e "${DIM}          $*${RESET}"; }

# ---------------------------------------------------------------------------
# Help
# ---------------------------------------------------------------------------

usage() {
    cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Inspect IncusOS node health

${BOLD}USAGE${RESET}
    ${SCRIPT_NAME} [OPTIONS] ACTION

${BOLD}ACTIONS${RESET}
    -s, --status        Quick status overview (hostname, version, uptime,
                        memory, TPM, Secure Boot, trusted state, update)
    -p, --partitions    Storage and partition info (drives, ZFS pools,
                        volumes, encryption status)
    -t, --tpm           TPM and security details (TPM status, Secure Boot,
                        certificates, recovery keys, trusted state)
    -S, --services      Service configuration (iscsi, lvm, multipath, nvme,
                        ovn, tailscale, usbip; OVN details)
    -N, --network       Network configuration (interfaces, addresses, routes,
                        state, DNS)
    -u, --update        Update configuration and state (channel, frequency,
                        auto reboot, versions, last check)
    -a, --all           Run all of the above

${BOLD}OPTIONS${RESET}
    -r, --remote NAME   Incus remote (default: ${REMOTE})
    --node NAME         Specific node (default: all nodes in cluster)
    -j, --json          Output raw JSON instead of formatted text
    -n, --dry-run       Show what would be queried without executing
    -v, --verbose       Show detailed output
    -q, --quiet         Suppress informational output
    -h, --help          Show this help message

${BOLD}API ENDPOINTS${RESET}
    /os/1.0                     Environment (hostname, version, uptime)
    /os/1.0/system/security     TPM, Secure Boot, encryption
    /os/1.0/system/storage      Drives, ZFS pools, volumes
    /os/1.0/system/resources    CPU, memory
    /os/1.0/system/network      Interfaces, DNS
    /os/1.0/system/update       Update config and state
    /os/1.0/services/SERVICE    Per-service configuration

${BOLD}EXAMPLES${RESET}
    # Quick status of all cluster nodes
    ${SCRIPT_NAME} --status

    # Storage info for a specific node
    ${SCRIPT_NAME} --partitions --node oc-node-02

    # All info as JSON
    ${SCRIPT_NAME} --all --json

    # Different remote
    ${SCRIPT_NAME} --status --remote lab-node-01

    # Preview queries without running
    ${SCRIPT_NAME} --all --dry-run
EOF
}

# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -s|--status)      ACTION="status";;
            -p|--partitions)  ACTION="partitions";;
            -t|--tpm)         ACTION="tpm";;
            -S|--services)    ACTION="services";;
            -N|--network)     ACTION="network";;
            -u|--update)      ACTION="update";;
            -a|--all)         ACTION="all";;
            -r|--remote)      REMOTE="$2"; shift;;
            --node)           NODE="$2"; shift;;
            -j|--json)        JSON_OUTPUT=true;;
            -n|--dry-run)     DRY_RUN=true;;
            -v|--verbose)     VERBOSE=true;;
            -q|--quiet)       QUIET=true;;
            -h|--help)        usage; exit 0;;
            *)
                error "Unknown option: $1"
                echo "Run '${SCRIPT_NAME} --help' for usage." >&2
                exit 1
                ;;
        esac
        shift
    done

    if [[ -z "$ACTION" ]]; then
        error "No action specified"
        echo "Run '${SCRIPT_NAME} --help' for usage." >&2
        exit 1
    fi
}

# ---------------------------------------------------------------------------
# Node discovery
# ---------------------------------------------------------------------------

discover_nodes() {
    if [[ -n "$NODE" ]]; then
        echo "$NODE"
        return
    fi

    local members
    members=$(incus cluster list "${REMOTE}:" -f csv 2>/dev/null \
        | cut -d',' -f1 \
        | sed 's/^ *//;s/ *$//') || true

    if [[ -z "$members" ]]; then
        # Not a cluster or cannot list -- fall back to the remote itself
        echo "$REMOTE"
        return
    fi

    echo "$members"
}

# ---------------------------------------------------------------------------
# Query helper
# ---------------------------------------------------------------------------

query_node() {
    local node="$1" endpoint="$2"

    if [[ "$DRY_RUN" == true ]]; then
        info "(dry-run) incus query ${node}:${endpoint}"
        return 0
    fi

    detail "incus query ${node}:${endpoint}"
    incus query "${node}:${endpoint}" 2>/dev/null || echo "{}"
}

# ---------------------------------------------------------------------------
# Section: status
# ---------------------------------------------------------------------------

show_status() {
    local node="$1"

    local env_json resources_json security_json update_json
    env_json=$(query_node "$node" "/os/1.0")
    resources_json=$(query_node "$node" "/os/1.0/system/resources")
    security_json=$(query_node "$node" "/os/1.0/system/security")
    update_json=$(query_node "$node" "/os/1.0/system/update")

    if [[ "$DRY_RUN" == true ]]; then return; fi

    if [[ "$JSON_OUTPUT" == true ]]; then
        python3 -c "
import json, sys
print(json.dumps({
    'environment': json.loads(sys.argv[1]),
    'resources': json.loads(sys.argv[2]),
    'security': json.loads(sys.argv[3]),
    'update': json.loads(sys.argv[4]),
}, indent=2))
" "$env_json" "$resources_json" "$security_json" "$update_json"
        return
    fi

    python3 -c "
import json, sys

env = json.loads(sys.argv[1]).get('environment', json.loads(sys.argv[1]))
res = json.loads(sys.argv[2])
sec_raw = json.loads(sys.argv[3])
sec = sec_raw.get('state', sec_raw)
upd = json.loads(sys.argv[4])

hostname = env.get('hostname', 'unknown')
version = env.get('os_version', env.get('version', 'unknown'))
uptime_s = env.get('uptime', 0)

# Format uptime
days = uptime_s // 86400
hours = (uptime_s % 86400) // 3600
mins = (uptime_s % 3600) // 60
parts = []
if days: parts.append(f'{days}d')
if hours: parts.append(f'{hours}h')
parts.append(f'{mins}m')
uptime_str = ' '.join(parts)

# Memory
mem = res.get('memory', {})
mem_used = mem.get('used', 0)
mem_total = mem.get('total', 0)

def fmt_bytes(b):
    for unit in ['B','KiB','MiB','GiB','TiB']:
        if b < 1024:
            return f'{b:.1f} {unit}'
        b /= 1024
    return f'{b:.1f} PiB'

mem_pct = (mem_used / mem_total * 100) if mem_total else 0

# Security (state level)
tpm_status = sec.get('tpm_status', 'unknown')
secboot_status = 'enabled' if sec.get('secure_boot_enabled', False) else 'disabled'
trusted = sec.get('system_state_is_trusted', 'unknown')

# Update
needs_reboot = upd.get('state', {}).get('needs_reboot', False)
last_check = upd.get('state', {}).get('last_check', 'never')

print(f'  Hostname:       {hostname}')
print(f'  Version:        {version}')
print(f'  Uptime:         {uptime_str}')
print(f'  Memory:         {fmt_bytes(mem_used)} / {fmt_bytes(mem_total)} ({mem_pct:.0f}%)')
print(f'  TPM:            {tpm_status}')
print(f'  Secure Boot:    {secboot_status}')
print(f'  System trusted: {trusted}')
print(f'  Needs reboot:   {\"yes\" if needs_reboot else \"no\"}')
print(f'  Last update:    {last_check}')
" "$env_json" "$resources_json" "$security_json" "$update_json"
}

# ---------------------------------------------------------------------------
# Section: partitions
# ---------------------------------------------------------------------------

show_partitions() {
    local node="$1"

    local storage_json security_json
    storage_json=$(query_node "$node" "/os/1.0/system/storage")
    security_json=$(query_node "$node" "/os/1.0/system/security")

    if [[ "$DRY_RUN" == true ]]; then return; fi

    if [[ "$JSON_OUTPUT" == true ]]; then
        python3 -c "
import json, sys
print(json.dumps({
    'storage': json.loads(sys.argv[1]),
    'security': json.loads(sys.argv[2]),
}, indent=2))
" "$storage_json" "$security_json"
        return
    fi

    python3 -c "
import json, sys

stor_raw = json.loads(sys.argv[1])
sec_raw = json.loads(sys.argv[2])
stor = stor_raw.get('state', stor_raw)
sec = sec_raw.get('state', sec_raw)

def fmt_bytes(b):
    for unit in ['B','KiB','MiB','GiB','TiB']:
        if b < 1024:
            return f'{b:.1f} {unit}'
        b /= 1024
    return f'{b:.1f} PiB'

# Drives
disks = stor.get('drives', stor.get('disks', []))
if disks:
    print('  Drives:')
    for d in disks:
        model = d.get('model_name', d.get('model', 'unknown'))
        bus = d.get('bus', 'unknown')
        size = d.get('capacity_in_bytes', d.get('size', 0))
        boot = ' [boot]' if d.get('boot') else ''
        did = d.get('id', '?').split('/')[-1]
        pool = d.get('member_pool', '')
        pool_str = f' pool={pool}' if pool else ''
        print(f'    {did}: {model} ({bus}, {fmt_bytes(size)}){boot}{pool_str}')
else:
    print('  Drives: none found')

# ZFS pools
pools = stor.get('pools', [])
if pools:
    print('  ZFS pools:')
    for p in pools:
        name = p.get('name', 'unknown')
        state = p.get('state', 'unknown')
        ptype = p.get('type', 'unknown')
        alloc = p.get('pool_allocated_space_in_bytes', 0)
        total = p.get('raw_pool_size_in_bytes', p.get('usable_pool_size_in_bytes', 0))
        pct = (alloc / total * 100) if total else 0
        enc_key = p.get('encryption_key_status', 'N/A')
        print(f'    {name}: {state} ({ptype}, {fmt_bytes(alloc)} / {fmt_bytes(total)}, {pct:.0f}%) enc_key={enc_key}')
        for v in p.get('volumes', []):
            vname = v.get('name', '?')
            vuse = v.get('use', '?')
            vusage = v.get('usage_in_bytes', 0)
            vquota = v.get('quota_in_bytes', 0)
            quota_str = fmt_bytes(vquota) if vquota else 'no quota'
            print(f'      vol {vname} ({vuse}): {fmt_bytes(vusage)} / {quota_str}')
else:
    print('  ZFS pools: none found')

# Encryption
encrypted_vols = sec.get('encrypted_volumes', [])
if encrypted_vols:
    print('  Encryption:')
    for v in encrypted_vols:
        vol = v.get('volume', '?')
        state = v.get('state', '?')
        print(f'    {vol}: {state}')
else:
    print('  Encryption: not configured')
" "$storage_json" "$security_json"
}

# ---------------------------------------------------------------------------
# Section: tpm
# ---------------------------------------------------------------------------

show_tpm() {
    local node="$1"

    local security_json
    security_json=$(query_node "$node" "/os/1.0/system/security")

    if [[ "$DRY_RUN" == true ]]; then return; fi

    if [[ "$JSON_OUTPUT" == true ]]; then
        python3 -c "
import json, sys
print(json.dumps(json.loads(sys.argv[1]), indent=2))
" "$security_json"
        return
    fi

    python3 -c "
import json, sys

sec_raw = json.loads(sys.argv[1])
sec = sec_raw.get('state', sec_raw)
sec_cfg = sec_raw.get('config', {})

# TPM
tpm_status = sec.get('tpm_status', 'unknown')
print(f'  TPM status:     {tpm_status}')

# Secure Boot
sb_enabled = sec.get('secure_boot_enabled', False)
print(f'  Secure Boot:    {\"enabled\" if sb_enabled else \"disabled\"}')

# Certificates
certs = sec.get('secure_boot_certificates', [])
if certs:
    print('  Certificates:')
    for c in certs:
        ctype = c.get('type', '?')
        subject = c.get('subject', 'unknown')
        fp = c.get('fingerprint', '')
        fp_short = fp[:16] + '...' if len(fp) > 16 else fp
        print(f'    {ctype:4s} {subject}')
        if fp_short:
            print(f'         fingerprint: {fp_short}')

# Encrypted volumes
enc_vols = sec.get('encrypted_volumes', [])
if enc_vols:
    print('  Encrypted volumes:')
    for v in enc_vols:
        print(f'    {v.get(\"volume\", \"?\")}: {v.get(\"state\", \"?\")}')

# Recovery keys
recovery_keys = sec_cfg.get('encryption_recovery_keys', [])
retrieved = sec.get('encryption_recovery_keys_retrieved', False)
print(f'  Recovery keys:  {len(recovery_keys)} key(s), retrieved={retrieved}')

# Pool recovery keys
pool_keys = sec.get('pool_recovery_keys', {})
if pool_keys:
    print(f'  Pool keys:      {list(pool_keys.keys())}')

# System trusted
trusted = sec.get('system_state_is_trusted', 'unknown')
print(f'  System trusted: {trusted}')
" "$security_json"
}

# ---------------------------------------------------------------------------
# Section: services
# ---------------------------------------------------------------------------

show_services() {
    local node="$1"

    if [[ "$JSON_OUTPUT" == true ]]; then
        local all_json="{"
        local first=true
        for svc in "${SERVICES[@]}"; do
            local svc_json
            svc_json=$(query_node "$node" "/os/1.0/services/${svc}")
            if [[ "$DRY_RUN" == true ]]; then continue; fi
            if [[ "$first" == true ]]; then
                first=false
            else
                all_json+=","
            fi
            all_json+="\"${svc}\":${svc_json}"
        done
        all_json+="}"
        if [[ "$DRY_RUN" != true ]]; then
            echo "$all_json" | python3 -m json.tool
        fi
        return
    fi

    for svc in "${SERVICES[@]}"; do
        local svc_json
        svc_json=$(query_node "$node" "/os/1.0/services/${svc}")
        if [[ "$DRY_RUN" == true ]]; then continue; fi

        python3 -c "
import json, sys

svc_name = sys.argv[1]
data = json.loads(sys.argv[2])

config = data.get('config', data)
enabled = config.get('enabled', False)
status = 'enabled' if enabled else 'disabled'

line = f'  {svc_name:12s} {status}'
print(line)

# Extra details for OVN
if svc_name == 'ovn' and enabled:
    db = config.get('database', '')
    tunnel_addr = config.get('tunnel_address', '')
    tunnel_proto = config.get('tunnel_protocol', '')
    if db:
        print(f'               database:        {db}')
    if tunnel_addr:
        print(f'               tunnel address:  {tunnel_addr}')
    if tunnel_proto:
        print(f'               tunnel protocol: {tunnel_proto}')
" "$svc" "$svc_json"
    done
}

# ---------------------------------------------------------------------------
# Section: network
# ---------------------------------------------------------------------------

show_network() {
    local node="$1"

    local network_json
    network_json=$(query_node "$node" "/os/1.0/system/network")

    if [[ "$DRY_RUN" == true ]]; then return; fi

    if [[ "$JSON_OUTPUT" == true ]]; then
        echo "$network_json" | python3 -m json.tool
        return
    fi

    python3 -c "
import json, sys

net = json.loads(sys.argv[1])

def fmt_bytes(b):
    for unit in ['B','KiB','MiB','GiB','TiB']:
        if b < 1024: return f'{b:.1f} {unit}'
        b /= 1024
    return f'{b:.1f} PiB'

# Interfaces
ifaces = net.get('interfaces', net.get('config', {}).get('interfaces', []))
if not isinstance(ifaces, list): ifaces = []
print('  Interfaces:')
for iface in ifaces:
    name = iface.get('name', 'unknown')
    hwaddr = iface.get('hwaddr', iface.get('mac', ''))
    roles = iface.get('roles', [])
    roles_str = ', '.join(roles) if isinstance(roles, list) and roles else ''
    addrs = iface.get('addresses', [])
    addr_strs = [a if isinstance(a, str) else a.get('address', '?') for a in addrs]
    state = iface.get('state', {})

    header = f'    {name}'
    if hwaddr: header += f' ({hwaddr})'
    if roles_str: header += f'  [{roles_str}]'
    print(header)

    if addr_strs:
        print(f'      addresses: {\", \".join(addr_strs)}')

    link_state = state.get('state', state.get('link', ''))
    mtu = state.get('mtu', iface.get('mtu', ''))
    speed = state.get('speed', '')
    if link_state:
        parts = [f'state={link_state}']
        if mtu: parts.append(f'mtu={mtu}')
        if speed: parts.append(f'speed={speed}')
        print(f'      link:      {\", \".join(parts)}')

    routes = iface.get('routes', [])
    if isinstance(routes, list) and routes:
        route_strs = []
        for r in routes:
            if isinstance(r, str): route_strs.append(r)
            elif isinstance(r, dict):
                s = r.get('destination', r.get('to', '?'))
                via = r.get('via', r.get('gateway', ''))
                if via: s += f' via {via}'
                route_strs.append(s)
        if route_strs:
            print(f'      routes:    {route_strs[0]}')
            for rs in route_strs[1:]:
                print(f'                 {rs}')

    stats = state.get('stats', iface.get('stats', {}))
    if isinstance(stats, dict) and stats:
        rx = stats.get('rx_bytes', stats.get('rx', 0))
        tx = stats.get('tx_bytes', stats.get('tx', 0))
        if rx or tx:
            print(f'      stats:     rx={fmt_bytes(rx)}, tx={fmt_bytes(tx)}')

# DNS
dns = net.get('dns', net.get('config', {}).get('dns', {}))
if isinstance(dns, dict) and dns:
    print('  DNS:')
    hostname = dns.get('hostname', dns.get('search', ''))
    nameservers = dns.get('nameservers', dns.get('servers', []))
    if hostname: print(f'    hostname:    {hostname}')
    if isinstance(nameservers, list) and nameservers:
        print(f'    nameservers: {\", \".join(str(n) for n in nameservers)}')
" "$network_json"
}

# ---------------------------------------------------------------------------
# Section: update
# ---------------------------------------------------------------------------

show_update() {
    local node="$1"

    local update_json env_json
    update_json=$(query_node "$node" "/os/1.0/system/update")
    env_json=$(query_node "$node" "/os/1.0")

    if [[ "$DRY_RUN" == true ]]; then return; fi

    if [[ "$JSON_OUTPUT" == true ]]; then
        echo "$update_json" | python3 -m json.tool
        return
    fi

    python3 -c "
import json, sys

upd = json.loads(sys.argv[1])
env = json.loads(sys.argv[2]).get('environment', json.loads(sys.argv[2]))
config = upd.get('config', {})
state = upd.get('state', {})

channel = config.get('channel', 'unknown')
frequency = config.get('check_frequency', 'unknown')
auto_reboot = config.get('auto_reboot', 'unknown')

current = env.get('os_version', 'unknown')
next_ver = env.get('os_version_next', 'none')
update_status = state.get('status', '')
last_check = state.get('last_check', 'never')
needs_reboot = state.get('needs_reboot', False)

print(f'  Channel:        {channel}')
print(f'  Check freq:     {frequency}')
print(f'  Auto reboot:    {auto_reboot}')
print(f'  Current:        {current}')
print(f'  Next version:   {next_ver}')
print(f'  Update status:  {update_status}')
print(f'  Last check:     {last_check}')
print(f'  Needs reboot:   {\"yes\" if needs_reboot else \"no\"}')
" "$update_json" "$env_json"
}

# ---------------------------------------------------------------------------
# Run a section across all nodes
# ---------------------------------------------------------------------------

run_section() {
    local section_name="$1"
    local section_fn="$2"
    shift 2
    local nodes=("$@")

    step "$section_name"

    for node in "${nodes[@]}"; do
        echo -e "  ${BOLD}--- ${node} ---${RESET}"
        "$section_fn" "$node"
        echo
    done
}

run_section_json() {
    local section_fn="$1"
    shift
    local nodes=("$@")

    if [[ ${#nodes[@]} -eq 1 ]]; then
        "$section_fn" "${nodes[0]}"
    else
        echo "{"
        local first=true
        for node in "${nodes[@]}"; do
            if [[ "$first" == true ]]; then
                first=false
            else
                echo ","
            fi
            echo "  \"${node}\":"
            "$section_fn" "$node" | sed 's/^/  /'
        done
        echo "}"
    fi
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

main() {
    setup_colors
    parse_args "$@"

    # Discover cluster nodes
    if [[ "$DRY_RUN" == true ]]; then
        if [[ -n "$NODE" ]]; then
            info "(dry-run) Would query node: ${NODE}"
        else
            info "(dry-run) Would discover nodes via: incus cluster list ${REMOTE}: -f csv"
        fi
    fi

    local nodes_raw
    nodes_raw=$(discover_nodes)

    local nodes=()
    while IFS= read -r line; do
        [[ -n "$line" ]] && nodes+=("$line")
    done <<< "$nodes_raw"

    if [[ ${#nodes[@]} -eq 0 ]]; then
        error "No nodes found. Check that remote '${REMOTE}' is accessible."
        error "  incus remote list"
        exit 1
    fi

    if [[ "$DRY_RUN" != true ]]; then
        info "Nodes: ${nodes[*]}"
    fi
    echo

    # Section lookup: action -> (title, function)
    local -A SECTION_TITLE=(
        [status]="Status" [partitions]="Storage & Partitions"
        [tpm]="TPM & Security" [services]="Services"
        [network]="Network" [update]="Update"
    )

    # Build section list
    local sections=()
    if [[ "$ACTION" == "all" ]]; then
        sections=(status partitions tpm services network update)
    else
        sections=("$ACTION")
    fi

    # Dispatch
    if [[ "$JSON_OUTPUT" == true && ${#sections[@]} -gt 1 ]]; then
        echo "{"
        local i=0
        for section in "${sections[@]}"; do
            echo "  \"${section}\":"
            run_section_json "show_${section}" "${nodes[@]}" | sed 's/^/  /'
            i=$((i + 1))
            [[ $i -lt ${#sections[@]} ]] && echo ","
        done
        echo "}"
    else
        for section in "${sections[@]}"; do
            if [[ "$JSON_OUTPUT" == true ]]; then
                run_section_json "show_${section}" "${nodes[@]}"
            else
                run_section "${SECTION_TITLE[$section]}" "show_${section}" "${nodes[@]}"
            fi
        done
    fi
}

main "$@"
