#!/usr/bin/env bash
# yonder — set up and run Yonder on this PC.
#
# Installed as /usr/bin/yonder by the package. Run `yonder help` for usage.
#
# Paths and unit names can be overridden (environment first, then
# /etc/riftlink/yonder.env as KEY=VALUE lines) for a machine that installs the
# daemon some other way:
#   YONDER_DAEMON  YONDER_TMUX  YONDER_UNIT  YONDER_SOCKET_UNIT  YONDER_CONFIG
#   YONDER_PORT    YONDER_MANAGED (non-empty: who manages this box; the
#                  commands that install units refuse to run)
set -euo pipefail

YONDER_ENV_FILE="${YONDER_ENV_FILE:-/etc/riftlink/yonder.env}"

# KEY=VALUE only, known keys only: this file is read by root, so it is parsed,
# never sourced.
load_env_file() {
    local f="$1" line k v
    [ -r "$f" ] || return 0
    while IFS= read -r line || [ -n "$line" ]; do
        case "$line" in '' | \#*) continue ;; esac
        k="${line%%=*}"
        v="${line#*=}"
        v="${v%\"}"; v="${v#\"}"
        case "$k" in
            YONDER_DAEMON | YONDER_TMUX | YONDER_UNIT | YONDER_SOCKET_UNIT | YONDER_CONFIG | \
                YONDER_PORT | YONDER_MANAGED | YONDER_TEMPLATE | YONDER_VERSION_FILE)
                [ -n "${!k:-}" ] || printf -v "$k" '%s' "$v" ;;
        esac
    done <"$f"
}
load_env_file "$YONDER_ENV_FILE"

YONDER_DAEMON="${YONDER_DAEMON:-/usr/lib/yonder/riftlinkd}"
YONDER_TMUX="${YONDER_TMUX:-/usr/lib/yonder/bin/tmux}"
YONDER_UNIT="${YONDER_UNIT:-yonder.service}"
YONDER_SOCKET_UNIT="${YONDER_SOCKET_UNIT:-yonder.socket}"
YONDER_CONFIG="${YONDER_CONFIG:-/etc/riftlink/riftlinkd.toml}"
YONDER_TEMPLATE="${YONDER_TEMPLATE:-/usr/lib/yonder/riftlinkd.toml.in}"
YONDER_VERSION_FILE="${YONDER_VERSION_FILE:-/usr/lib/yonder/VERSION}"
YONDER_PORT="${YONDER_PORT:-9091}"
YONDER_MANAGED="${YONDER_MANAGED:-}"
DROPIN_DIR="/etc/systemd/system/${YONDER_UNIT}.d"
OWNER_CONF="$DROPIN_DIR/owner.conf"
DESK_LOGIN_UNIT="riftlink-desk-login.service"
DESK_LOGIN_DROPIN_DIR="/etc/systemd/system/${DESK_LOGIN_UNIT}.d"
CF_DIR="/etc/yonder/cloudflared"
CF_UNIT="yonder-cloudflared.service"

# ── Output ──────────────────────────────────────────────────────────────────
if [ -t 1 ]; then
    c_ok=$'\e[32m' c_warn=$'\e[33m' c_err=$'\e[31m' c_dim=$'\e[2m' c_off=$'\e[0m'
else
    c_ok='' c_warn='' c_err='' c_dim='' c_off=''
fi
die() { printf '%syonder: %s%s\n' "$c_err" "$*" "$c_off" >&2; exit 1; }
say() { printf '%s\n' "$*"; }
note() { printf '%s%s%s\n' "$c_dim" "$*" "$c_off"; }

need_root() {
    [ "$(id -u)" -eq 0 ] || die "this needs root: sudo yonder $*"
}
refuse_root() {
    [ "$(id -u)" -ne 0 ] || die "run this as the desk's owner, not as root (the daemon refuses root too)."
}
refuse_managed() {
    [ -z "$YONDER_MANAGED" ] || die "this machine's Yonder is managed by $YONDER_MANAGED; \`yonder $1\` would fight it."
}
have() { command -v "$1" >/dev/null 2>&1; }
has_systemd() { [ -d /run/systemd/system ]; }

# ── TOML ────────────────────────────────────────────────────────────────────
# Just enough TOML for the keys this script owns: string values of plain
# `key = "value"` lines inside a `[section]`. Anything else in the file is left
# byte-for-byte alone.

# toml_get FILE SECTION KEY — print the string value (empty when unset).
toml_get() {
    SEC="$2" KEY="$3" awk '
        function header(s) { sub(/^[ \t]*\[+[ \t]*/, "", s); sub(/[ \t]*\]+.*$/, "", s); return s }
        /^[ \t]*\[/ { in_sec = ($0 !~ /^[ \t]*\[\[/) && header($0) == ENVIRON["SEC"]; next }
        in_sec {
            line = $0
            if (match(line, "^[ \t]*" ENVIRON["KEY"] "[ \t]*=[ \t]*")) {
                v = substr(line, RLENGTH + 1)
                if (v ~ /^"/) {
                    v = substr(v, 2); out = ""
                    while (length(v) > 0) {
                        ch = substr(v, 1, 1)
                        if (ch == "\\") { out = out substr(v, 2, 1); v = substr(v, 3); continue }
                        if (ch == "\"") break
                        out = out ch; v = substr(v, 2)
                    }
                    print out
                } else {
                    sub(/[ \t]*(#.*)?$/, "", v); print v
                }
                exit
            }
        }' "$1"
}

# toml_set FILE SECTION KEY VALUE — set KEY to the string VALUE in [SECTION]:
# replace the line if the key is there, else add it right under the section
# header, else append the section. Atomic, keeps the file's owner and mode.
toml_set() {
    local file="$1" sec="$2" key="$3" val="$4" tmp esc
    esc="${val//\\/\\\\}"
    esc="${esc//\"/\\\"}"
    case "$key" in *[!A-Za-z0-9_-]* | '') die "toml_set: bad key $key" ;; esac
    tmp="$(mktemp "$(dirname "$file")/.yonder-toml.XXXXXX")"
    if toml_has_key "$file" "$sec" "$key"; then
        SEC="$sec" KEY="$key" LINE="$key = \"$esc\"" awk '
            function header(s) { sub(/^[ \t]*\[+[ \t]*/, "", s); sub(/[ \t]*\]+.*$/, "", s); return s }
            /^[ \t]*\[/ { in_sec = ($0 !~ /^[ \t]*\[\[/) && header($0) == ENVIRON["SEC"]; print; next }
            in_sec && !done && $0 ~ ("^[ \t]*" ENVIRON["KEY"] "[ \t]*=") { print ENVIRON["LINE"]; done = 1; next }
            { print }' "$file" >"$tmp"
    elif toml_has_section "$file" "$sec"; then
        SEC="$sec" LINE="$key = \"$esc\"" awk '
            function header(s) { sub(/^[ \t]*\[+[ \t]*/, "", s); sub(/[ \t]*\]+.*$/, "", s); return s }
            { print }
            !done && /^[ \t]*\[/ && $0 !~ /^[ \t]*\[\[/ && header($0) == ENVIRON["SEC"] {
                print ENVIRON["LINE"]; done = 1
            }' "$file" >"$tmp"
    else
        {
            cat "$file"
            [ ! -s "$file" ] || [ -z "$(tail -c1 "$file")" ] || echo
            printf '\n[%s]\n%s = "%s"\n' "$sec" "$key" "$esc"
        } >"$tmp"
    fi
    chmod --reference="$file" "$tmp" 2>/dev/null || true
    chown --reference="$file" "$tmp" 2>/dev/null || true
    mv -f "$tmp" "$file"
}

toml_has_section() {
    SEC="$2" awk '
        function header(s) { sub(/^[ \t]*\[+[ \t]*/, "", s); sub(/[ \t]*\]+.*$/, "", s); return s }
        /^[ \t]*\[/ && $0 !~ /^[ \t]*\[\[/ && header($0) == ENVIRON["SEC"] { found = 1; exit }
        END { exit !found }' "$1"
}

toml_has_key() {
    SEC="$2" KEY="$3" awk '
        function header(s) { sub(/^[ \t]*\[+[ \t]*/, "", s); sub(/[ \t]*\]+.*$/, "", s); return s }
        /^[ \t]*\[/ { in_sec = ($0 !~ /^[ \t]*\[\[/) && header($0) == ENVIRON["SEC"]; next }
        in_sec && $0 ~ ("^[ \t]*" ENVIRON["KEY"] "[ \t]*=") { found = 1; exit }
        END { exit !found }' "$1"
}

# ── Local HTTP ──────────────────────────────────────────────────────────────
# GET http://127.0.0.1:PORT$1 → body on stdout; non-zero when unreachable or
# not 2xx. curl when present, else bash's /dev/tcp (minimal installs).
http_get() {
    local path="$1"
    if have curl; then
        curl -fsS -m 5 "http://127.0.0.1:$YONDER_PORT$path" 2>/dev/null
        return
    fi
    local resp
    resp="$(timeout 5 bash -c '
        exec 3<>"/dev/tcp/127.0.0.1/$1" || exit 1
        printf "GET %s HTTP/1.0\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n" "$2" >&3
        cat <&3' _ "$YONDER_PORT" "$path" 2>/dev/null)" || return 1
    case "${resp%%$'\r'*}" in
        "HTTP/1."?" 2"*) printf '%s' "${resp#*$'\r\n\r\n'}" ;;
        *) return 1 ;;
    esac
}

# json_str JSON KEY — a top-level-ish scalar from a small JSON object.
json_str() {
    printf '%s' "$1" | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",}]*\).*/\1/p" | head -1
}

# health_report JSON — what /api/health says beyond "ok", one line each:
#   tmux <version> <ok:true|false>
#   warn <text>
# (Older daemons report neither; then this prints nothing.)
health_report() {
    if have python3; then
        printf '%s' "$1" | python3 -c '
import json, sys
try:
    h = json.load(sys.stdin)
except ValueError:
    sys.exit(0)
t = h.get("tmux")
if isinstance(t, dict):
    print("tmux %s %s" % (t.get("version") or "?", "true" if t.get("ok") else "false"))
for w in h.get("warnings") or []:
    print("warn %s" % (w if isinstance(w, str) else json.dumps(w)))
' 2>/dev/null || true
    else
        local v ok
        v="$(printf '%s' "$1" | sed -n 's/.*"tmux"[[:space:]]*:[[:space:]]*{[^}]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
        ok="$(printf '%s' "$1" | sed -n 's/.*"tmux"[[:space:]]*:[[:space:]]*{[^}]*"ok"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p')"
        [ -n "$v" ] && echo "tmux $v ${ok:-true}"
    fi
    return 0
}

wait_healthy() {
    local _
    for _ in $(seq 1 "${1:-20}"); do
        http_get /api/health >/dev/null && return 0
        sleep 1
    done
    return 1
}

# ── Owner ───────────────────────────────────────────────────────────────────
# The account the service runs as, from the drop-in `yonder enable` wrote.
service_owner() {
    local u=""
    if [ -r "$OWNER_CONF" ]; then
        u="$(sed -n 's/^User=//p' "$OWNER_CONF" | tail -1)"
    fi
    if [ -z "$u" ] && have systemctl; then
        u="$(systemctl show -p User --value "$YONDER_UNIT" 2>/dev/null || true)"
    fi
    printf '%s' "$u"
}

passwd_field() { getent passwd "$1" | cut -d: -f"$2"; }

restart_if_running() {
    has_systemd || return 0
    if systemctl is-active --quiet "$YONDER_UNIT" || systemctl is-enabled --quiet "$YONDER_SOCKET_UNIT" 2>/dev/null; then
        systemctl restart "$YONDER_UNIT"
        wait_healthy 20 || say "${c_warn}The daemon did not come back; see: yonder logs${c_off}"
    fi
}

# ── enable ──────────────────────────────────────────────────────────────────
cmd_enable() {
    local user="${SUDO_USER:-}" force=0 linger=1
    while [ $# -gt 0 ]; do
        case "$1" in
            --user) [ $# -ge 2 ] || die "--user needs a name"; user="$2"; shift 2 ;;
            --user=*) user="${1#--user=}"; shift ;;
            --force) force=1; shift ;;
            --no-linger) linger=0; shift ;;
            *) die "enable: unknown argument $1" ;;
        esac
    done
    need_root enable
    refuse_managed enable
    [ -n "$user" ] || die "whose desk is this? Run it with sudo from your own account, or pass --user NAME."
    [ "$user" != "root" ] || die "the owner can't be root: Yonder gives the owner's own shell to their phone."
    getent passwd "$user" >/dev/null || die "no such user: $user"

    local current
    current="$(service_owner)"
    if [ -n "$current" ] && [ "$current" != "$user" ] && [ "$current" != "root" ] && [ "$force" = 0 ]; then
        die "this PC already belongs to $current (one owner per machine). Use --force to hand it to $user."
    fi

    local uid gid group home shell
    uid="$(passwd_field "$user" 3)"
    gid="$(passwd_field "$user" 4)"
    home="$(passwd_field "$user" 6)"
    shell="$(passwd_field "$user" 7)"
    group="$(getent group "$gid" | cut -d: -f1)"
    [ -n "$group" ] || group="$gid"
    case "$shell" in
        '' | */nologin | */false) shell="/bin/bash" ;;
    esac
    [ -x "$YONDER_DAEMON" ] || die "the daemon is missing ($YONDER_DAEMON). Reinstall the yonder package."

    say "Setting up Yonder for $user (home $home, shell $shell)"

    # Groups: `yonder` for the polkit power rule, `input` for /dev/uinput.
    if ! getent group yonder >/dev/null; then
        if have systemd-sysusers && [ -f /usr/lib/sysusers.d/yonder.conf ]; then
            systemd-sysusers yonder.conf >/dev/null 2>&1 || true
        fi
        getent group yonder >/dev/null || groupadd --system yonder
    fi
    local g
    for g in yonder input; do
        if getent group "$g" >/dev/null; then
            id -nG "$user" | tr ' ' '\n' | grep -qx "$g" || usermod -aG "$g" "$user"
        fi
    done

    install -d -o "$user" -g "$gid" -m 0750 /var/lib/riftlink /var/log/riftlink
    chown "$user:$gid" /var/lib/riftlink /var/log/riftlink

    # Config: written once, never overwritten.
    install -d -m 0755 "$(dirname "$YONDER_CONFIG")"
    if [ ! -e "$YONDER_CONFIG" ]; then
        [ -r "$YONDER_TEMPLATE" ] || die "config template missing: $YONDER_TEMPLATE"
        local t tmp
        t="$(cat "$YONDER_TEMPLATE")"
        t="${t//@HOME@/$home}"
        t="${t//@SHELL@/$shell}"
        t="${t//@USER@/$user}"
        tmp="$(mktemp "$(dirname "$YONDER_CONFIG")/.riftlinkd.toml.XXXXXX")"
        printf '%s\n' "$t" >"$tmp"
        chown "root:$gid" "$tmp"
        chmod 0640 "$tmp"
        mv -f "$tmp" "$YONDER_CONFIG"
        say "  wrote $YONDER_CONFIG"
    else
        say "  kept $YONDER_CONFIG"
        # The daemon reads it as the owner.
        if ! runuser -u "$user" -- test -r "$YONDER_CONFIG" 2>/dev/null; then
            chgrp "$gid" "$YONDER_CONFIG" && chmod g+r "$YONDER_CONFIG"
        fi
    fi

    # The owner's desktop session. Unknown at install time and only guessable
    # now: take the sockets that exist, else the usual names.
    local wl="wayland-0" x=":0" s
    for s in /run/user/"$uid"/wayland-[0-9]*; do
        case "$s" in *.lock) continue ;; esac
        [ -S "$s" ] && { wl="$(basename "$s")"; break; }
    done
    for s in /tmp/.X11-unix/X[0-9]*; do
        [ -S "$s" ] && [ "$(stat -c %u "$s" 2>/dev/null)" = "$uid" ] && { x=":${s##*/X}"; break; }
    done
    install -d -m 0755 "$DROPIN_DIR"
    {
        echo "# Written by \`yonder enable\`: who owns this desk, and their desktop session."
        echo "# Re-run \`sudo yonder enable\` to regenerate it."
        echo "[Service]"
        echo "User=$user"
        echo "Group=$gid"
        echo "Environment=XDG_RUNTIME_DIR=/run/user/$uid"
        echo "Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus"
        echo "Environment=WAYLAND_DISPLAY=$wl"
        echo "Environment=DISPLAY=$x"
        if [ -f "$home/.Xauthority" ]; then
            echo "Environment=\"XAUTHORITY=$home/.Xauthority\""
        fi
    } >"$OWNER_CONF.new"
    chmod 0644 "$OWNER_CONF.new"
    mv -f "$OWNER_CONF.new" "$OWNER_CONF"
    say "  wrote $OWNER_CONF"

    # "Log in" from the app (starts the desktop from the login screen) and
    # restart/shut down/sleep: the helper unit learns whose desktop to start;
    # polkit lets group `yonder` (added above) start it and use the power actions.
    if [ -f "/usr/lib/systemd/system/$DESK_LOGIN_UNIT" ] || [ -n "${YONDER_DESK_LOGIN_FORCE:-}" ]; then
        install -d -m 0755 "$DESK_LOGIN_DROPIN_DIR"
        printf '# Written by `yonder enable`.\n[Service]\nEnvironment=RIFTLINK_USER=%s\n' "$user" \
            >"$DESK_LOGIN_DROPIN_DIR/owner.conf"
        chmod 0644 "$DESK_LOGIN_DROPIN_DIR/owner.conf"
        say "  desk login + power from the app: enabled for $user"
    fi

    if ! has_systemd; then
        say ""
        say "systemd is not running here, so nothing was started. Start the daemon yourself:"
        say "  runuser -u $user -- env PATH=$(dirname "$YONDER_TMUX"):/usr/bin:/bin $YONDER_DAEMON $YONDER_CONFIG"
        return 0
    fi

    # Linger starts the owner's user manager at boot: the daemon then has a
    # session bus and can keep tmux in its own scope before anyone logs in.
    if [ "$linger" = 1 ] && have loginctl; then
        loginctl enable-linger "$user" 2>/dev/null || note "  (could not enable linger for $user)"
    fi
    if have udevadm; then
        modprobe uinput 2>/dev/null || true
        udevadm trigger --sysname-match=uinput 2>/dev/null || true
    fi
    systemctl daemon-reload
    systemctl enable --now "$YONDER_SOCKET_UNIT" >/dev/null
    systemctl enable "$YONDER_UNIT" >/dev/null 2>&1 || true
    systemctl restart "$YONDER_UNIT"
    if wait_healthy 30; then
        say "${c_ok}Yonder is running on http://localhost:$YONDER_PORT${c_off}"
    else
        die "the daemon did not answer on 127.0.0.1:$YONDER_PORT. See: yonder logs"
    fi
    offer_tailnet
    open_desk_browser "$user" "$uid" "$wl" "$x" "$home"
    next_steps "$user"
}

# Tailscale is already up here and the PC has no address yet: offer the tunnel
# now, so the first code a phone scans already works. Asked only at a terminal.
offer_tailnet() {
    [ -t 0 ] && [ -t 1 ] || return 0
    have tailscale && tailscale status >/dev/null 2>&1 || return 0
    [ -z "$(toml_get "$YONDER_CONFIG" server tailnet_origin)" ] || return 0
    [ -z "$(public_origin)" ] || return 0
    say ""
    printf 'Tailscale is on. Let your phone reach this PC through it? [Y/n] '
    local a=""
    read -r a || return 0
    case "$a" in [nN]*) return 0 ;; esac
    # A subshell: tunnel_tailscale exits on failure, and enable has still worked.
    (tunnel_tailscale) || note "  Skipped. Later: sudo yonder tunnel tailscale"
}

# The account is made in a browser on this PC: open it there, in the owner's
# desktop session, as the Mac and Windows installers do.
open_desk_browser() {
    local user="$1" uid="$2" wl="$3" x="$4" home="$5" url="http://localhost:$YONDER_PORT"
    [ "$(json_str "$(http_get /api/auth/status 2>/dev/null || true)" has_user)" = "true" ] && return 0
    have xdg-open || return 0
    [ -S "/run/user/$uid/$wl" ] || [ -S "/tmp/.X11-unix/X${x#:}" ] || return 0
    local xauth=""
    [ -f "$home/.Xauthority" ] && xauth="XAUTHORITY=$home/.Xauthority"
    # setsid: the browser must outlive this command (and a package manager's).
    setsid -f runuser -u "$user" -- env XDG_RUNTIME_DIR="/run/user/$uid" \
        DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
        WAYLAND_DISPLAY="$wl" DISPLAY="$x" $xauth xdg-open "$url" >/dev/null 2>&1 </dev/null || return 0
    say "  Opened $url in your browser: create your account there."
}

next_steps() {
    local has_user reach=0
    has_user="$(json_str "$(http_get /api/auth/status 2>/dev/null || true)" has_user)"
    { [ -n "$(toml_get "$YONDER_CONFIG" server tailnet_origin)" ] || [ -n "$(public_origin)" ]; } && reach=1
    say ""
    say "Next:"
    if [ "$has_user" = "true" ]; then
        say "  Link a device: http://localhost:$YONDER_PORT on this PC, Settings > Link a device (or: yonder pair)."
    else
        say "  In the browser on this PC (http://localhost:$YONDER_PORT): create your account, then scan"
        say "  the code it shows with your phone."
    fi
    if [ "$reach" = 0 ]; then
        say "  On the same Wi-Fi: in that browser, choose Allow phones on this Wi-Fi."
        say "  From anywhere: install Tailscale here and on the phone, then run"
        say "  sudo yonder tunnel tailscale  (or: tunnel cloudflare). The code on screen updates by itself."
    fi
}

# ── disable ─────────────────────────────────────────────────────────────────
cmd_disable() {
    need_root disable
    refuse_managed disable
    if has_systemd; then
        systemctl disable --now "$YONDER_SOCKET_UNIT" "$YONDER_UNIT" 2>/dev/null || true
        if systemctl list-unit-files "$CF_UNIT" >/dev/null 2>&1 && systemctl is-enabled --quiet "$CF_UNIT" 2>/dev/null; then
            systemctl disable --now "$CF_UNIT" || true
        fi
    fi
    say "Yonder is stopped and will not start at boot."
    note "Your account, paired devices and $YONDER_CONFIG are kept; \`sudo yonder enable\` turns it back on."
    note "Open terminal sessions keep running in tmux."
}

# ── setup / pair ────────────────────────────────────────────────────────────
check_invoker_is_owner() {
    local owner me
    owner="$(service_owner)"
    me="$(id -un)"
    if [ -n "$owner" ] && [ "$owner" != "root" ] && [ "$owner" != "$me" ]; then
        die "Yonder on this PC runs as $owner; run this as $owner."
    fi
}

cmd_setup() {
    refuse_root
    check_invoker_is_owner
    [ -r "$YONDER_CONFIG" ] || die "no readable config at $YONDER_CONFIG. Run: sudo yonder enable"
    exec "$YONDER_DAEMON" setup --config "$YONDER_CONFIG" "$@"
}

cmd_pair() {
    refuse_root
    check_invoker_is_owner
    [ -r "$YONDER_CONFIG" ] || die "no readable config at $YONDER_CONFIG. Run: sudo yonder enable"
    http_get /api/health >/dev/null || die "the daemon isn't answering on 127.0.0.1:$YONDER_PORT. Run: sudo yonder enable"
    exec "$YONDER_DAEMON" pair --config "$YONDER_CONFIG" "$@"
}

cmd_reset() {
    refuse_root
    check_invoker_is_owner
    [ -r "$YONDER_CONFIG" ] || die "no readable config at $YONDER_CONFIG. Run: sudo yonder enable"
    exec "$YONDER_DAEMON" reset --config "$YONDER_CONFIG" "$@"
}

# ── tunnel ──────────────────────────────────────────────────────────────────
tailscale_dnsname() {
    local js
    js="$(tailscale status --json 2>/dev/null)" || return 1
    if have python3; then
        printf '%s' "$js" | python3 -c 'import json,sys; print(json.load(sys.stdin)["Self"]["DNSName"])' 2>/dev/null && return 0
    fi
    if have jq; then
        printf '%s' "$js" | jq -r '.Self.DNSName' && return 0
    fi
    # Self's DNSName is the first one inside the "Self" object.
    printf '%s' "$js" | sed -n '/"Self"[[:space:]]*:/,$p' | grep -m1 '"DNSName"' |
        sed 's/.*"DNSName"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/'
}

# Point browsers' origin (WebAuthn + CORS + the WebSocket origin check) at a
# tunnel address, unless a public origin already holds that role.
set_browser_origin() {
    local origin="$1" host="${1#https://}"
    toml_set "$YONDER_CONFIG" auth webauthn_rp_origin "$origin"
    toml_set "$YONDER_CONFIG" auth webauthn_rp_id "${host%%[:/]*}"
}

cmd_tunnel() {
    local kind="${1:-}"
    [ $# -gt 0 ] && shift
    case "$kind" in
        tailscale) tunnel_tailscale "$@" ;;
        cloudflare) tunnel_cloudflare "$@" ;;
        *) die "usage: yonder tunnel tailscale | yonder tunnel cloudflare --hostname HOST" ;;
    esac
}

tunnel_tailscale() {
    [ $# -eq 0 ] || die "tunnel tailscale takes no arguments"
    need_root tunnel tailscale
    [ -w "$YONDER_CONFIG" ] || die "no config at $YONDER_CONFIG. Run: sudo yonder enable"
    have tailscale || die "Tailscale isn't installed. Get it from https://tailscale.com/download, run \`sudo tailscale up\`, then try again."
    tailscale status >/dev/null 2>&1 || die "Tailscale isn't connected. Run \`sudo tailscale up\` first."
    local dns
    dns="$(tailscale_dnsname || true)"
    dns="${dns%.}"
    [ -n "$dns" ] || die "Tailscale reports no DNS name for this PC. Turn on MagicDNS in the Tailscale admin console."

    say "Serving Yonder on your tailnet at https://$dns ..."
    note "(If HTTPS isn't enabled for your tailnet yet, Tailscale prints a link to enable it.)"
    # Current CLI (1.52+) first, then the older `serve https / <target>` form.
    if ! tailscale serve --bg "http://127.0.0.1:$YONDER_PORT"; then
        tailscale serve https / "http://127.0.0.1:$YONDER_PORT" ||
            die "tailscale serve failed. Check \`tailscale serve status\`."
    fi

    local origin="https://$dns"
    toml_set "$YONDER_CONFIG" server tailnet_origin "$origin"
    if [ -z "$(public_origin)" ]; then
        set_browser_origin "$origin"
    fi
    restart_if_running
    say "${c_ok}Tailnet address: $origin${c_off}"
    say "Devices on your tailnet can reach this PC now. Pair one with: yonder pair"
}

tunnel_cloudflare() {
    local hostname="" name=""
    while [ $# -gt 0 ]; do
        case "$1" in
            --hostname) [ $# -ge 2 ] || die "--hostname needs a value"; hostname="$2"; shift 2 ;;
            --hostname=*) hostname="${1#--hostname=}"; shift ;;
            --name) [ $# -ge 2 ] || die "--name needs a value"; name="$2"; shift 2 ;;
            --name=*) name="${1#--name=}"; shift ;;
            *) die "tunnel cloudflare: unknown argument $1" ;;
        esac
    done
    need_root tunnel cloudflare
    refuse_managed "tunnel cloudflare"
    [ -n "$hostname" ] || die "usage: sudo yonder tunnel cloudflare --hostname desk.example.com (a name in a domain on your Cloudflare account)"
    case "$hostname" in
        *[!A-Za-z0-9.-]* | .* | *. | *..*) die "not a host name: $hostname" ;;
        *.*) ;;
        *) die "not a fully qualified host name: $hostname" ;;
    esac
    [ -w "$YONDER_CONFIG" ] || die "no config at $YONDER_CONFIG. Run: sudo yonder enable"
    have cloudflared || die "cloudflared isn't installed. See https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
    name="${name:-yonder-$(hostname -s 2>/dev/null || echo desk)}"

    install -d -m 0700 "$CF_DIR"
    local cert="$CF_DIR/cert.pem"
    if [ ! -s "$cert" ]; then
        say "Step 1/3: log in to Cloudflare and pick the domain for $hostname."
        note "(cloudflared prints a link; open it in any browser.)"
        local home_cert="${HOME:-/root}/.cloudflared/cert.pem"
        cloudflared tunnel login || die "cloudflared login failed"
        [ -s "$home_cert" ] || die "cloudflared did not leave a certificate at $home_cert"
        install -m 0600 "$home_cert" "$cert"
    fi
    local cf=(cloudflared --origincert "$cert")

    say "Step 2/3: tunnel \"$name\""
    local id creds
    id="$("${cf[@]}" tunnel list --output json --name "$name" 2>/dev/null |
        grep -oE '"id"[[:space:]]*:[[:space:]]*"[0-9a-f-]{36}"' | head -1 | grep -oE '[0-9a-f-]{36}' || true)"
    if [ -z "$id" ]; then
        "${cf[@]}" tunnel create --credentials-file "$CF_DIR/$name.json" "$name" ||
            die "could not create the tunnel"
        id="$("${cf[@]}" tunnel list --output json --name "$name" |
            grep -oE '"id"[[:space:]]*:[[:space:]]*"[0-9a-f-]{36}"' | head -1 | grep -oE '[0-9a-f-]{36}')"
    fi
    [ -n "$id" ] || die "could not find the tunnel id for $name"
    creds="$CF_DIR/$name.json"
    [ -s "$creds" ] || die "missing tunnel credentials $creds (the tunnel exists but was created elsewhere; delete it with \`cloudflared tunnel delete $name\` and retry)"
    chmod 0600 "$creds"

    say "Step 3/3: DNS $hostname -> tunnel"
    "${cf[@]}" tunnel route dns --overwrite-dns "$id" "$hostname" ||
        die "could not route $hostname to the tunnel"

    cat >"$CF_DIR/config.yml" <<EOF
# Written by \`yonder tunnel cloudflare\`.
tunnel: $id
credentials-file: $creds
ingress:
  - hostname: $hostname
    service: http://127.0.0.1:$YONDER_PORT
  - service: http_status:404
EOF
    cat >"/etc/systemd/system/$CF_UNIT" <<EOF
# Written by \`yonder tunnel cloudflare\`: the Cloudflare Tunnel to this desk.
[Unit]
Description=Yonder Cloudflare Tunnel ($hostname)
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=0

[Service]
ExecStart=$(command -v cloudflared) --no-autoupdate --config $CF_DIR/config.yml tunnel run
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
    local origin="https://$hostname"
    toml_set "$YONDER_CONFIG" server public_origin "$origin"
    set_browser_origin "$origin"
    if has_systemd; then
        systemctl daemon-reload
        systemctl enable "$CF_UNIT" >/dev/null 2>&1
        systemctl restart "$CF_UNIT"
    fi
    restart_if_running
    say "${c_ok}Public address: $origin${c_off}"
    say "Pair a device with: yonder pair"
    note "Anyone can reach the sign-in page there; only paired or desk-approved devices get in."
}

# The public origin as the daemon derives it: server.public_origin, else an
# https webauthn_rp_origin on a non-local host.
public_origin() {
    local o
    o="$(toml_get "$YONDER_CONFIG" server public_origin)"
    if [ -z "$o" ]; then
        o="$(toml_get "$YONDER_CONFIG" auth webauthn_rp_origin)"
        case "$o" in
            https://localhost* | https://127.* | https://\[::1\]*) o="" ;;
            https://*) ;;
            *) o="" ;;
        esac
    fi
    printf '%s' "${o%/}"
}

# ── status ──────────────────────────────────────────────────────────────────
yonder_version() {
    if [ -r "$YONDER_VERSION_FILE" ]; then cat "$YONDER_VERSION_FILE"; else echo "unknown"; fi
}

cmd_status() {
    local health ver build owner st has_user o
    say "Yonder $(yonder_version)"
    owner="$(service_owner)"
    if has_systemd; then
        st="$(systemctl is-active "$YONDER_UNIT" 2>/dev/null || true)"
        say "  service:   $YONDER_UNIT $st, $(systemctl is-enabled "$YONDER_SOCKET_UNIT" 2>/dev/null || echo disabled) at boot"
    fi
    say "  owner:     ${owner:-none (run: sudo yonder enable)}"
    if health="$(http_get /api/health)"; then
        ver="$(json_str "$health" version)"
        build="$(json_str "$health" build)"
        say "  daemon:    answering on 127.0.0.1:$YONDER_PORT (Yonder $ver, build $build)"
        local kind rest
        while read -r kind rest; do
            case "$kind" in
                tmux) say "  tmux:      ${rest% *}$([ "${rest##* }" = true ] || echo ' (TOO OLD: needs 3.5+)')" ;;
                warn) say "  ${c_warn}warning:${c_off}   $rest" ;;
            esac
        done < <(health_report "$health")
        has_user="$(json_str "$(http_get /api/auth/status || true)" has_user)"
        case "$has_user" in
            true) say "  account:   created" ;;
            false) say "  account:   none yet (run: yonder setup)" ;;
            *) say "  account:   unknown" ;;
        esac
    else
        say "  daemon:    not answering on 127.0.0.1:$YONDER_PORT"
    fi
    if [ -r "$YONDER_CONFIG" ]; then
        say "  addresses:"
        say "    local    http://localhost:$YONDER_PORT"
        o="$(toml_get "$YONDER_CONFIG" server tailnet_origin)"
        [ -n "$o" ] && say "    tailnet  $o"
        o="$(public_origin)"
        [ -n "$o" ] && say "    public   $o"
        say "  browser origin: $(toml_get "$YONDER_CONFIG" auth webauthn_rp_origin)"
    else
        say "  config:    $YONDER_CONFIG not readable here"
    fi
    note "(Paired devices and the daemon's own view of its addresses need sign-in; see the app.)"
}

# ── doctor ──────────────────────────────────────────────────────────────────
n_warn=0 n_fail=0
ok() { printf '  %sok%s    %s\n' "$c_ok" "$c_off" "$*"; }
warn() { printf '  %swarn%s  %s\n' "$c_warn" "$c_off" "$*"; n_warn=$((n_warn + 1)); }
fail() { printf '  %sFAIL%s  %s\n' "$c_err" "$c_off" "$*"; n_fail=$((n_fail + 1)); }

# version_ge A B — true when dotted version A >= B (letters suffixes ignored).
version_ge() {
    [ "$(printf '%s\n%s\n' "$2" "$1" | sed 's/[a-z]*$//' | sort -V | head -1)" = "$(printf '%s' "$2" | sed 's/[a-z]*$//')" ]
}

cmd_doctor() {
    local owner uid t v p real first
    owner="$(service_owner)"
    say "Yonder $(yonder_version) — checking this PC"

    say "Daemon"
    if [ -x "$YONDER_DAEMON" ]; then ok "daemon: $YONDER_DAEMON"; else fail "daemon missing: $YONDER_DAEMON"; fi
    # A stale copy earlier on PATH makes `riftlinkd pair` run old code (it once
    # read "pair" as a config path).
    real="$(readlink -f "$YONDER_DAEMON" 2>/dev/null || echo "$YONDER_DAEMON")"
    first="$(type -P riftlinkd 2>/dev/null || true)"
    if [ -n "$first" ] && [ "$(readlink -f "$first")" != "$real" ]; then
        warn "\`riftlinkd\` on your PATH is $first ($(date -r "$first" +%F 2>/dev/null)), not $YONDER_DAEMON — use \`yonder pair\`/\`yonder setup\`, or remove the stale copy"
    fi
    first="$(type -P yonder 2>/dev/null || true)"
    if [ -n "$first" ] && [ "$(readlink -f "$first")" != "$(readlink -f "$0" 2>/dev/null || echo "$0")" ]; then
        warn "\`yonder\` on your PATH is $first, not this one ($0)"
    fi
    if [ -n "$owner" ] && [ "$owner" != "root" ]; then ok "owner: $owner"; else fail "no owner yet: sudo yonder enable"; fi
    if has_systemd; then
        if systemctl is-active --quiet "$YONDER_UNIT"; then ok "$YONDER_UNIT active"
        elif systemctl is-active --quiet "$YONDER_SOCKET_UNIT"; then ok "$YONDER_SOCKET_UNIT listening (daemon starts on first connection)"
        else fail "$YONDER_UNIT is not running (sudo yonder enable; yonder logs)"; fi
    else
        warn "systemd is not running (container?) — the service can't be managed here"
    fi
    local health="" kind rest
    if health="$(http_get /api/health)"; then
        ok "answers on 127.0.0.1:$YONDER_PORT (Yonder $(json_str "$health" version))"
        while read -r kind rest; do
            case "$kind" in
                tmux)
                    if [ "${rest##* }" = true ]; then ok "the daemon's tmux: ${rest% *}"
                    else fail "the daemon runs tmux ${rest% *}, older than 3.5 (check PATH in the unit)"; fi ;;
                warn) warn "daemon: $rest" ;;
            esac
        done < <(health_report "$health")
    else
        fail "nothing answers on 127.0.0.1:$YONDER_PORT"
    fi
    if [ -r "$YONDER_CONFIG" ]; then ok "config: $YONDER_CONFIG"
    elif [ -e "$YONDER_CONFIG" ]; then warn "config exists but isn't readable by $(id -un)"
    else fail "no config: sudo yonder enable"; fi
    for p in /var/lib/riftlink /var/log/riftlink; do
        if [ -d "$p" ]; then
            t="$(stat -c %U "$p")"
            if [ -n "$owner" ] && [ "$t" != "$owner" ]; then fail "$p is owned by $t, not $owner (sudo chown -R $owner: $p)"; else ok "$p"; fi
        else
            fail "$p missing: sudo yonder enable"
        fi
    done

    say "Terminal"
    if [ -x "$YONDER_TMUX" ] || t="$(type -P "$YONDER_TMUX")"; then
        v="$("$YONDER_TMUX" -V 2>/dev/null | awk '{print $2}')"
        if [ -n "$v" ] && version_ge "$v" 3.5; then ok "tmux $v ($YONDER_TMUX)"
        else fail "tmux ${v:-?} at $YONDER_TMUX is older than 3.5 (paste needs paste-buffer -S)"; fi
        t="$(type -P tmux 2>/dev/null || true)"
        if [ -n "$t" ] && [ "$(readlink -f "$t")" != "$(readlink -f "$(type -P "$YONDER_TMUX")")" ]; then
            local tv
            tv="$(tmux -V 2>/dev/null | awk '{print $2}')"
            [ "$tv" = "$v" ] || note "        your shell's tmux is $t ($tv); to attach to Yonder's sessions use \`yonder attach\`"
        fi
    else
        fail "tmux missing: $YONDER_TMUX"
    fi

    say "Desktop (optional: picture, mouse and keyboard)"
    local session_type="" desktop=""
    if [ -n "$owner" ] && have loginctl; then
        local sid
        for sid in $(loginctl list-sessions --no-legend 2>/dev/null | awk -v u="$owner" '$3 == u {print $1}'); do
            t="$(loginctl show-session "$sid" -p Type --value 2>/dev/null)"
            case "$t" in wayland | x11) session_type="$t"; desktop="$(loginctl show-session "$sid" -p Desktop --value 2>/dev/null)"; break ;; esac
        done
    fi
    [ -n "$session_type" ] || session_type="${XDG_SESSION_TYPE:-}"
    [ -n "$desktop" ] || desktop="${XDG_CURRENT_DESKTOP:-}"
    case "$session_type" in
        wayland) ok "session: Wayland${desktop:+ ($desktop)}" ;;
        x11) ok "session: X11${desktop:+ ($desktop)}" ;;
        *) warn "no graphical session found for ${owner:-the owner} (fine for terminal-only use)" ;;
    esac
    if have ffmpeg; then ok "ffmpeg $(ffmpeg -version 2>/dev/null | awk 'NR==1{print $3}')"; else warn "ffmpeg missing: no desktop picture"; fi
    if have python3; then
        if python3 -c 'import gi; gi.require_version("Gio", "2.0"); from gi.repository import Gio, GLib' 2>/dev/null; then
            ok "python3 + PyGObject (portal input)"
        else
            warn "python3 has no PyGObject (gi): no mouse/keyboard on Wayland (install python3-gi / python3-gobject / python-gobject)"
        fi
    else
        warn "python3 missing: no mouse/keyboard on Wayland"
    fi
    local portal=""
    for p in /usr/libexec/xdg-desktop-portal /usr/lib/xdg-desktop-portal /usr/lib/xdg-desktop-portal/xdg-desktop-portal; do
        [ -x "$p" ] && { portal="$p"; break; }
    done
    if [ -n "$portal" ]; then
        local backends
        backends="$(ls /usr/share/xdg-desktop-portal/portals/ 2>/dev/null | sed 's/\.portal$//' | tr '\n' ' ')"
        ok "xdg-desktop-portal (${backends:-no backends})"
        case "$backends" in
            *kde* | *gnome* | *wlr* | *hyprland*) ;;
            *) [ "$session_type" = wayland ] && warn "no KDE/GNOME/wlr portal backend: screen capture and input need one" ;;
        esac
    else
        [ "$session_type" = wayland ] && warn "xdg-desktop-portal missing: no picture or input on Wayland" || note "        xdg-desktop-portal not installed"
    fi
    if have gst-inspect-1.0 && gst-inspect-1.0 pipewiresrc >/dev/null 2>&1; then ok "GStreamer pipewiresrc"; fi
    if [ -e /dev/uinput ]; then
        t="$(stat -c '%G %a' /dev/uinput)"
        if [ "${t%% *}" = "input" ]; then ok "/dev/uinput (group input)"; else warn "/dev/uinput is ${t} (expected group input; try: sudo udevadm trigger --sysname-match=uinput)"; fi
        if [ -n "$owner" ] && getent group input >/dev/null; then
            if id -nG "$owner" | tr ' ' '\n' | grep -qx input; then ok "$owner is in group input"
            else warn "$owner isn't in group input (sudo yonder enable adds it)"; fi
        fi
    else
        warn "/dev/uinput missing (sudo modprobe uinput): no virtual mouse"
    fi
    if [ -n "$owner" ] && getent group yonder >/dev/null; then
        if id -nG "$owner" | tr ' ' '\n' | grep -qx yonder; then ok "$owner may restart/shut down from the app (group yonder)"
        else warn "$owner isn't in group yonder: power actions will ask for a password"; fi
    fi
    if [ -f "$DESK_LOGIN_DROPIN_DIR/owner.conf" ]; then
        ok "\"Log in\" from the app: ready ($DESK_LOGIN_UNIT)"
    elif [ -f "/usr/lib/systemd/system/$DESK_LOGIN_UNIT" ]; then
        warn "\"Log in\" from the app isn't set up: sudo yonder enable"
    fi
    if ! have sddm && ! have gdm && ! have gdm3 && [ ! -x /usr/sbin/gdm3 ] && [ ! -x /usr/bin/sddm ]; then
        note "        no SDDM or GDM found: \"Log in\" from the app needs one of them"
    fi

    say "Reaching this PC"
    local o tunnel=0
    if [ -r "$YONDER_CONFIG" ]; then
        o="$(toml_get "$YONDER_CONFIG" server tailnet_origin)"; [ -n "$o" ] && { ok "tailnet: $o"; tunnel=1; }
        o="$(public_origin)"; [ -n "$o" ] && { ok "public: $o"; tunnel=1; }
    fi
    # The daemon also finds its tailnet address by itself when Tailscale is up.
    if [ "$tunnel" = 0 ] && ! { have tailscale && tailscale status >/dev/null 2>&1; }; then
        warn "no tunnel yet: only this PC can connect (sudo yonder tunnel tailscale)"
    fi
    if have tailscale; then
        if tailscale status >/dev/null 2>&1; then ok "tailscale connected"; else warn "tailscale installed but not connected"; fi
    fi
    if have cloudflared; then ok "cloudflared $(cloudflared --version 2>/dev/null | awk '{print $3}')"; fi

    say ""
    if [ "$n_fail" -gt 0 ]; then say "${c_err}$n_fail problem(s), $n_warn warning(s).${c_off}"; return 1; fi
    if [ "$n_warn" -gt 0 ]; then say "${c_warn}$n_warn warning(s).${c_off}"; else say "${c_ok}All good.${c_off}"; fi
}

# ── tmux helpers ────────────────────────────────────────────────────────────
tmux_socket() {
    local dir=""
    [ -r "$YONDER_CONFIG" ] && dir="$(toml_get "$YONDER_CONFIG" terminal tmux_socket_dir)"
    printf '%s/riftlink.sock' "${dir:-/tmp/riftlink-tmux}"
}

# Yonder's sessions live on their own tmux server; a client of another tmux
# version can't talk to it, so always go through the bundled binary.
cmd_attach() {
    refuse_root
    if [ $# -gt 0 ]; then
        exec "$YONDER_TMUX" -S "$(tmux_socket)" attach-session -t "=$1"
    fi
    exec "$YONDER_TMUX" -S "$(tmux_socket)" attach-session
}

cmd_tmux() {
    refuse_root
    exec "$YONDER_TMUX" -S "$(tmux_socket)" "$@"
}

cmd_logs() {
    have journalctl || die "journalctl isn't available here"
    if [ $# -eq 0 ]; then set -- -n 200 --no-pager; fi
    exec journalctl -u "$YONDER_UNIT" -u "$YONDER_SOCKET_UNIT" "$@"
}

usage() {
    cat <<EOF
Yonder $(yonder_version) — your desk, from yonder.

Usage: yonder COMMAND [ARGS]

Set up (once):
  sudo yonder enable [--user NAME]   make NAME (default: you) the owner and start Yonder
  yonder setup [--reset-password]    create your account on this PC (or open
                                     http://localhost:$YONDER_PORT in a browser here)
  sudo yonder tunnel tailscale       reach this PC over your tailnet
  sudo yonder tunnel cloudflare --hostname HOST
                                     reach this PC at HOST through your Cloudflare account
  yonder pair [--name DEVICE]        add a phone or another computer (QR code + code)
  yonder reset [--yes]               sign out every device and delete the account,
                                     to start over with setup

Day to day:
  yonder status                      service, version, account and addresses
  yonder doctor                      check everything Yonder relies on
  yonder logs [journalctl args]      the daemon's log (e.g. yonder logs -f)
  yonder attach [SESSION]            open one of Yonder's terminal sessions here
  yonder tmux ARGS...                run tmux against Yonder's sessions
  sudo yonder disable                stop Yonder and don't start it at boot
  yonder --version | help
EOF
}

main() {
    local cmd="${1:-help}"
    [ $# -gt 0 ] && shift
    case "$cmd" in
        enable) cmd_enable "$@" ;;
        disable) cmd_disable "$@" ;;
        setup) cmd_setup "$@" ;;
        pair) cmd_pair "$@" ;;
        reset) cmd_reset "$@" ;;
        tunnel) cmd_tunnel "$@" ;;
        status) cmd_status "$@" ;;
        doctor) cmd_doctor "$@" ;;
        logs) cmd_logs "$@" ;;
        attach) cmd_attach "$@" ;;
        tmux) cmd_tmux "$@" ;;
        -V | --version | version) say "yonder $(yonder_version)" ;;
        -h | --help | help) usage ;;
        *) usage >&2; exit 2 ;;
    esac
}

# Sourced by the tests for its functions only.
if [ "${YONDER_LIB:-}" = 1 ]; then
    return 0 2>/dev/null || true
fi
main "$@"
