#!/bin/sh
# mesh-hosts-render — render the fleet's /etc/hosts block from
# data/mesh-hosts.json (+ the daemon's discovered-IP overlay) and splice it in.
#
# Emits a marked, idempotently-replaceable block at the TOP of /etc/hosts (so it
# wins first-match resolution):
#     # >>> mesh-hosts (managed by smart-lan-router/bin/mesh-hosts-render)
#     10.0.0.118  apricot.lan apricot
#     10.9.0.2    apricot.wg
#     10.0.0.118  quinn.apricot.lan www.quinn.apricot.lan ...
#     ...
#     # <<< mesh-hosts
#
# Name semantics (see mesh-hosts.json _schema.naming):
#   <host>.lan + BARE <host>  -> current LAN IP (overlay over static seed).
#       Direct at home; via the tunnel when away (the daemon routes the LAN /24
#       through wg then) — so this is the right default everywhere.
#   <host>.wg                 -> mesh IP (explicit tunnel path).
#   bare name of a LAN-less host (fennel/yuzu) -> its wg IP.
#   services (mesh-hosts.json .services) -> the hosting host's current LAN IP.
#
# Adoption: on --install this also REMOVES (a) the legacy setup-lan-dns block and
# (b) any loose non-comment line that names a host/service this block manages —
# those are hand-maintained duplicates that go stale on DHCP drift. The program
# owns these names now; never hand-edit them in /etc/hosts.
#
# Usage:
#   mesh-hosts-render            # print the block to stdout (default; safe)
#   mesh-hosts-render --install  # splice/replace in /etc/hosts (needs root)
#   mesh-hosts-render --diff     # show what --install would change, no write
#
# Exit codes:
#   0  success (printed, or installed, or already up to date)
#   1  missing dependency / unlocatable or invalid JSON
#   2  --install needs root but it isn't available

set -eu

mode=print
case "${1:-}" in
    ""|--print) mode=print ;;
    --install)  mode=install ;;
    --diff)     mode=diff ;;
    *) echo "mesh-hosts-render: unknown arg '$1' (use --print|--install|--diff)" >&2; exit 1 ;;
esac

BEGIN='# >>> mesh-hosts (managed by smart-lan-router/bin/mesh-hosts-render)'
END='# <<< mesh-hosts'
# Legacy block this tool replaces — stripped on install so its stale (drifted)
# host entries don't shadow ours in first-match resolution.
LEGACY_BEGIN='# >>> LAN hosts — managed by setup-lan-dns.sh'
LEGACY_END='# <<< LAN hosts'
HOSTS_FILE=/etc/hosts

# --- locate data file, surviving symlink invocation ----------------------------
self=$0
while [ -L "$self" ]; do
    link=$(readlink "$self")
    case $link in
        /*) self=$link ;;
        *)  self=$(dirname "$self")/$link ;;
    esac
done
root=$(cd "$(dirname "$self")" && pwd)
while [ "$root" != "/" ] && [ ! -f "$root/data/mesh-hosts.json" ]; do
    root=$(dirname "$root")
done
data_file="$root/data/mesh-hosts.json"
[ -f "$data_file" ] || { echo "mesh-hosts-render: cannot locate data/mesh-hosts.json (from $self)" >&2; exit 1; }

command -v jq >/dev/null || { echo "mesh-hosts-render: jq not installed" >&2; exit 1; }
jq empty "$data_file" || { echo "mesh-hosts-render: invalid JSON in $data_file" >&2; exit 1; }

hide_homelan=$(jq -r '.dx.hide_homelan // false' "$data_file")

# Overlay: current LAN IPs discovered by the daemon (data/lan-state.json, a
# {name: ip} map) override the static `lan` seed, so records track DHCP drift.
overlay='{}'
state_file="$root/data/lan-state.json"
if [ -f "$state_file" ] && jq -e . "$state_file" >/dev/null 2>&1; then
    overlay=$(cat "$state_file")
fi

# Vantage: can THIS node reach LAN IPs at all? A node with its own LAN leg (or
# the roaming laptop, whose tunnel carries the LAN /24) renders bare names and
# services at LAN IPs; a mesh-only node (yuzu) must use wg IPs instead.
short=$(hostname 2>/dev/null | cut -d. -f1)
if command -v ip >/dev/null 2>&1; then
    local_ips=$(ip -o -4 addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
else
    local_ips=$(ifconfig 2>/dev/null | awk '/inet /{print $2}')
fi
ips_json=$(printf '%s\n' $local_ips | jq -R . | jq -s .)
reachlan=$(jq -r --arg h "$short" --argjson ips "$ips_json" '
    [ .hosts[] | . as $x
      | select( ($x.name == $h) or ($x.aliases | index($h))
                or ($x.lan != null and ($ips | index($x.lan)))
                or ($ips | index($x.wg)) )
      | (($x.lan != null) or ($x.class == "laptop")) ] | first // false
' "$data_file")

render_block() {
    printf '%s\n' "$BEGIN"
    printf '# Auto-generated from net-tools/data/mesh-hosts.json + lan-state.json — re-run to update.\n'
    printf '# bare/<host>.lan = current LAN IP (direct at home, tunnel when away) · <host>.wg = mesh IP\n'
    if [ "$hide_homelan" = "true" ]; then
        printf '# dx mode: homelan hidden (only cloud/DO hosts rendered). Data preserved for recovery.\n'
    fi
    # LAN records — current discovered IP (overlay) over the static seed. Bare
    # names live here only when THIS node can reach LAN IPs (vantage).
    jq -r --argjson ov "$overlay" --argjson reachlan "$reachlan" --argjson hide "$hide_homelan" '
        .hosts[]
        | select( if $hide then (.class == "cloud") else true end )
        | . as $h
        | (($ov[$h.name]) // $h.lan) as $lan
        | select($lan != null)
        | "\($lan)\t"
          + ((([$h.name] + ($h.aliases // [])) | map(. + ".lan")) | join(" "))
          + (if $reachlan then " " + (([$h.name] + ($h.aliases // [])) | join(" ")) else "" end)
    ' "$data_file"
    # Mesh (.wg) records — explicit tunnel path. Bare names land here for
    # LAN-less hosts always, and for ALL hosts when this node is mesh-only.
    jq -r --argjson reachlan "$reachlan" --argjson hide "$hide_homelan" '
        .hosts[]
        | select( if $hide then (.class == "cloud") else true end )
        | . as $h
        | "\($h.wg)\t"
          + ((([$h.name] + ($h.aliases // [])) | map(. + ".wg")) | join(" "))
          + (if ($h.lan == null) or ($reachlan | not)
             then " " + (([$h.name] + ($h.aliases // [])) | join(" ")) else "" end)
    ' "$data_file"
    # Service vhosts — the hosting host'\''s current LAN IP, or its wg IP from a
    # mesh-only vantage.
    jq -r --argjson ov "$overlay" --argjson reachlan "$reachlan" --argjson hide "$hide_homelan" '
        . as $d
        | ($d.services // {}) | to_entries[]
        | select(.key != "_note")
        | .key as $hname
        | ($d.hosts[] | select(.name == $hname) | select( if $hide then (.class == "cloud") else true end )) as $h
        | (if $reachlan then (($ov[$hname]) // $h.lan) else $h.wg end) as $addr
        | select($addr != null)
        | "\($addr)\t\(.value | join(" "))"
    ' "$data_file"
    printf '%s\n' "$END"
}

block=$(render_block)

if [ "$mode" = "print" ]; then
    printf '%s\n' "$block"
    exit 0
fi

# Every hostname our block manages (host names, .wg/.lan forms, aliases, services)
# — loose lines carrying any of these are stale hand-maintained duplicates and
# get adopted (removed; our block supersedes them).
managed=$(printf '%s\n' "$block" | awk '!/^#/ && NF >= 2 { for (i = 2; i <= NF; i++) print $i }' | sort -u | tr '\n' ' ')

# Compute the new /etc/hosts: drop our old block + the legacy setup-lan-dns
# block, then scrub managed names out of the loose remainder.
current=$(cat "$HOSTS_FILE" 2>/dev/null || true)
stripped=$(printf '%s\n' "$current" | awk -v b="$BEGIN" -v e="$END" -v lb="$LEGACY_BEGIN" -v le="$LEGACY_END" '
    $0 == b || $0 == lb { skip = 1 }
    skip != 1 { print }
    $0 == e || $0 == le { skip = 0 }
')
stripped=$(printf '%s\n' "$stripped" | awk -v names="$managed" '
    BEGIN { n = split(names, a, /[[:space:]]+/); for (i = 1; i <= n; i++) if (a[i] != "") set[a[i]] = 1 }
    /^[[:space:]]*#/ || NF < 2 { print; next }
    {
        kept = ""; removed = 0
        for (i = 2; i <= NF; i++) {
            if ($i in set) removed++
            else kept = kept " " $i
        }
        if (removed == 0) { print; next }   # untouched
        if (kept == "") next                # every name managed → fully adopted
        print $1 kept                       # partially adopted → keep the rest
    }
')
# Trim leading + trailing blank lines from the stripped body (the separators our
# previous splice left behind — without this, blanks accumulate and the install
# is never a no-op).
stripped=$(printf '%s\n' "$stripped" | awk 'NF {f=1; p=NR} f {l[NR]=$0} END {for(i=1;i<=p;i++) if (i in l) print l[i]}')
# PREPEND our block so its records win /etc/hosts first-match resolution over any
# other (e.g. a stale setup-lan-dns block that still lists a drifted apricot.lan).
new=$(printf '%s\n\n%s\n' "$block" "$stripped")

if [ "$mode" = "diff" ]; then
    if command -v diff >/dev/null 2>&1; then
        printf '%s\n' "$new" | diff -u "$HOSTS_FILE" - || true
    else
        printf '%s\n' "$new"
    fi
    exit 0
fi

# --install
if printf '%s\n' "$new" | cmp -s - "$HOSTS_FILE"; then
    echo "mesh-hosts-render: $HOSTS_FILE already up to date"
    exit 0
fi

SUDO=
if [ "$(id -u)" -ne 0 ]; then
    if command -v sudo >/dev/null 2>&1; then
        SUDO="sudo"
    else
        echo "mesh-hosts-render: --install needs root" >&2
        exit 2
    fi
fi

printf '%s\n' "$new" | $SUDO tee "$HOSTS_FILE" >/dev/null
echo "mesh-hosts-render: updated $HOSTS_FILE"
