#!/bin/bash

# omarchy:summary=Share keyboard and mouse with a nearby Omarchy machine
# omarchy:group=handoff
# omarchy:args=[status|toggle|on|off|position <edge>|peer <host> <ip>|fingerprint|pair <desc> <sha256>|restart|log [-f|N]|fixcase|doctor]
# omarchy:examples=omarchy-handoff position right
# omarchy:examples=omarchy-handoff toggle

# NOTE: the omarchy: metadata above is cosmetic. Omarchy's dispatcher only scans
# its own bin dir, so `omarchy handoff ...` does NOT work -- invoke this as the
# bare `omarchy-handoff`.

# HARD-WON FACTS -- kept here because lan-mouse rewrites config.toml on every
# CLI activate/deactivate (i.e. every SUPER+SHIFT+K) and strips its comments.
# Startup activation does NOT rewrite it; only the CLI path does.
#   * capture_backend MUST be "layer-shell". On Hyprland 0.56.2 the
#     "input-capture-portal" backend is a SILENT NO-OP: it initializes cleanly,
#     org.freedesktop.portal.InputCapture answers on D-Bus (v1, caps=3), and it
#     creates ZERO barriers. No edge ever fires and nothing is logged.
#     Verify with: lan-mouse --capture-backend <b> test-capture
#     and look for "adding capture for position X - using outputs: [NON-EMPTY]".
#     Note layer-shell logs an EMPTY pass for all positions first, then a
#     populated pass after enumerating outputs -- empty alone is not a failure.
#   * layer-shell arms barriers PER OUTPUT, not just the desktop's outer rim,
#     so an interior monitor edge is a valid crossing point.
#   * [authorized_fingerprints] is keyed by FINGERPRINT: "<sha256>" = "<desc>".
#     Reversed is a valid non-empty table, so paired() below CANNOT detect it;
#     auth then fails silently and looks exactly like a dropped packet.
#   * capture_backend/position/activate_on_startup/fingerprints all survive the
#     rewrite; only comments are lost.
#   * The daemon runs as the lan-mouse.service USER UNIT. Do NOT start it from
#     autostart.lua's o.launch_on_start(): that sends stdout AND stderr to
#     /dev/null, so every pairing failure is invisible. Cost: hours of
#     diagnosing from a stale pre-reboot log file while the live daemon logged
#     nowhere. `omarchy-handoff log` reads the journal for this reason.
#   * The log level env var is LAN_MOUSE_LOG_LEVEL, NOT RUST_LOG. RUST_LOG is
#     silently ignored, so it looks like debug logging "doesn't work".
#   * Transport is DTLS over UDP (webrtc-dtls), not QUIC. The error
#     "Alert is Fatal or Close Notify" (= ErrAlertFatalOrClose) means the REMOTE
#     sent a fatal alert, i.e. the PEER REJECTED OUR CERT. Something answered,
#     so it is a TRUST failure, never a firewall. Contrast "Connection timed
#     out" = nothing answered at all (firewall, or the source-address trap
#     below).
#   * The fingerprint lan-mouse compares is sha256 of the DER certificate --
#     exactly `openssl x509 -noout -fingerprint -sha256`. Not the PEM file hash,
#     not the pubkey hash.
#   * SOURCE-ADDRESS TRAP (two NICs on one subnet): the daemon listens on
#     0.0.0.0 but does not set IP_PKTINFO, so replies take the kernel's
#     route-chosen source. With the dock up, `ip route get <peer>` prefers the
#     dock NIC, so a peer connecting to our Wi-Fi address gets replies from the
#     dock address and drops them as an unknown 4-tuple -> "Connection timed
#     out" on that address only, while ICMP to it works fine. Undocked it
#     inverts and works. Listing both peer-side IPs is correct; the unusable
#     one just costs a 5s timeout per attempt.

# Thin wrapper over lan-mouse. lan-mouse's own config.toml is the single source
# of truth -- this script only drives its CLI so the edge and pairing are
# changeable without hand-editing TOML, and so a keybind has something to call.

set -euo pipefail

CONFIG="${CONFIG:-$HOME/.config/lan-mouse/config.toml}"
CERT="$HOME/.config/lan-mouse/lan-mouse.pem"
SOCKET="${XDG_RUNTIME_DIR:-/run/user/$UID}/lan-mouse-socket.sock"
LOG="${XDG_STATE_HOME:-$HOME/.local/state}/lan-mouse.log"

notify() { omarchy-notification-send -g "󰌌" "Handoff" "$1" 2>/dev/null || echo "$1"; }
die() { echo "omarchy-handoff: $1" >&2; exit 1; }

# The daemon owns the socket; every cli call is a no-op without it.
require_daemon() {
  [[ -S $SOCKET ]] || die "lan-mouse daemon is not running (try: omarchy-handoff restart)"
}

# Resolve a client's numeric id from its hostname, since lan-mouse addresses
# clients by an index that shifts as entries are added or removed.
client_id() {
  local host="${1:-}"
  require_daemon
  if [[ -n $host ]]; then
    lan-mouse cli list | sed -n "s/^id \([0-9]\+\): ${host}:.*/\1/p" | head -n1
  else
    lan-mouse cli list | sed -n 's/^id \([0-9]\+\):.*/\1/p' | head -n1
  fi
}

first_host() {
  require_daemon
  lan-mouse cli list | sed -n 's/^id [0-9]\+: \([^:]*\):.*/\1/p' | head -n1
}

cmd_status() {
  if [[ -S $SOCKET ]]; then
    echo "daemon:  running"
  else
    echo "daemon:  stopped"
    return 0
  fi
  echo "peers:"
  lan-mouse cli list | sed 's/^/  /'
  echo "fingerprint (this machine):"
  echo "  $(cmd_fingerprint)"
}

cmd_fingerprint() {
  [[ -f $CERT ]] || die "no certificate yet at $CERT (start the daemon once)"
  # MUST be lowercase. lan-mouse compares with a plain HashMap key lookup
  # (src/listen.rs: authorized.contains_key(&fingerprints[0])) against
  # crypto::generate_fingerprint(), which formats "{x:02x}" and .to_lowercase().
  # openssl prints UPPERCASE, so passing it through unchanged silently never
  # matches -- that was the real cause of the dead crossing on 2026-10-10.
  openssl x509 -in "$CERT" -noout -fingerprint -sha256 | cut -d= -f2 | tr "A-Z" "a-z"
}

# Activation is what actually gates input crossing the edge, so on/off/toggle
# map to activate/deactivate rather than stopping the daemon.
is_active() {
  local id="$1"
  lan-mouse cli list | grep -q "^id ${id}:.*active: true"
}

# Arming the edge with no authorized peer key is the one way this locks up a
# machine: input gets captured, the peer rejects it unauthenticated, and the
# keyboard looks dead. Refuse unless pairing is mutual, or --force is given.
# Parse the actual TOML table rather than slicing text: a line-range slice from
# the header to EOF also counts keys belonging to any table emitted AFTER
# [authorized_fingerprints], and `lan-mouse cli save-config` -- not us -- chooses
# that order. An empty fingerprint table followed by any populated table made the
# text version return true and arm an unpaired machine, i.e. it failed OPEN into
# exactly the lockup this guard exists to prevent.
# A malformed or unreadable config counts as 0, so it fails CLOSED.
paired() {
  [[ -f $CONFIG ]] || return 1
  local n
  n=$(python3 -I -c '
import sys, tomllib
try:
    with open(sys.argv[1], "rb") as f:
        print(len(tomllib.load(f).get("authorized_fingerprints", {})))
except (OSError, tomllib.TOMLDecodeError):
    print(0)
' "$CONFIG")
  (( n > 0 ))
}

cmd_on() {
  local force=0
  [[ ${1:-} == --force ]] && { force=1; shift; }
  local id; id=$(client_id "${1:-}")
  [[ -n $id ]] || die "no peer configured"
  if (( ! force )) && ! paired; then
    die "no peer fingerprint authorized -- refusing to capture input.
  Crossing the edge now would forward this keyboard and mouse to a peer that
  cannot accept it, and the machine would look frozen (release: Ctrl+Shift+Super+Alt).
  Pair first:  omarchy-handoff pair <name> <sha256>
  Override:    omarchy-handoff on --force"
  fi
  lan-mouse cli activate "$id"
  notify "Handoff enabled"
}
cmd_off() { local id; id=$(client_id "${1:-}"); [[ -n $id ]] || die "no peer configured"; lan-mouse cli deactivate "$id"; notify "Handoff disabled"; }

cmd_toggle() {
  local id; id=$(client_id "${1:-}")
  [[ -n $id ]] || die "no peer configured (try: omarchy-handoff peer <host> <ip>)"
  if is_active "$id"; then cmd_off "${1:-}"; else cmd_on "${1:-}"; fi
}

# The whole point of the wrapper: the physical side the other laptop sits on is
# one value, not a code change.
cmd_position() {
  local edge="${1:-}" id
  case "$edge" in
    left|right|top|bottom) ;;
    *) die "position must be one of: left right top bottom" ;;
  esac
  id=$(client_id "${2:-}")
  [[ -n $id ]] || die "no peer configured"
  lan-mouse cli set-position "$id" "$edge"
  lan-mouse cli save-config
  notify "Peer is now on the $edge"
}

cmd_peer() {
  local host="${1:-}" ip="${2:-}" edge="${3:-left}"
  [[ -n $host && -n $ip ]] || die "usage: omarchy-handoff peer <hostname> <ip>[,<ip>...] [edge]"
  require_daemon
  local existing; existing=$(client_id "$host")
  [[ -n $existing ]] && lan-mouse cli remove-client "$existing"
  lan-mouse cli add-client --hostname "$host" --ips "$ip" --port 4242
  cmd_position "$edge" "$host"
}

cmd_pair() {
  local desc="${1:-}" fp="${2:-}"
  # Normalise case: a fingerprint copied from openssl or a chat message is
  # usually uppercase, and lan-mouse's comparison is case-SENSITIVE.
  fp=$(tr "A-Z" "a-z" <<<"$fp")
  [[ -n $desc && -n $fp ]] || die "usage: omarchy-handoff pair <description> <sha256-fingerprint>"
  require_daemon
  lan-mouse cli authorize-key "$desc" "$fp"
  lan-mouse cli save-config
  notify "Authorized $desc"
}

cmd_fixcase() {
  [[ -f $CONFIG ]] || die "no config at $CONFIG"
  python3 -I -c "
import re, sys
path = sys.argv[1]
with open(path) as f:
    s = f.read()
pat = re.compile(\"\\\"([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){31})\\\"\\\\s*=\")
n = sum(1 for m in pat.finditer(s) if any(c.isupper() for c in m.group(1)))
s2 = pat.sub(lambda m: chr(34) + m.group(1).lower() + chr(34) + \" =\", s)
if n:
    with open(path, \"w\") as f:
        f.write(s2)
print(n)
" "$CONFIG" | {
    read -r n
    if (( n > 0 )); then
      echo "lowercased $n fingerprint(s); restart to load: omarchy-handoff restart"
    else
      echo "nothing to fix -- all fingerprints already lowercase"
    fi
  }
}

cmd_restart() {
  # Prefer the user unit, matching cmd_log's assumption. pkill + relaunch races
  # the unit's Restart=on-failure and can leave TWO daemons contending for UDP
  # 4242 and the socket, so only fall back to a hand-start when no unit exists.
  if systemctl --user --quiet is-enabled lan-mouse.service 2>/dev/null \
     || systemctl --user --quiet is-active lan-mouse.service 2>/dev/null; then
    systemctl --user restart lan-mouse.service
  else
    pkill -x lan-mouse 2>/dev/null || true
    mkdir -p "$(dirname "$LOG")"
    # Keep the log. Discarding it cost real debugging time once: the portal
    # backend's silent no-op is invisible without these lines.
    if command -v uwsm-app >/dev/null; then
      uwsm-app -- lan-mouse daemon >>"$LOG" 2>&1 &
    else
      setsid lan-mouse daemon >>"$LOG" 2>&1 &
    fi
  fi
  sleep 2
  [[ -S $SOCKET ]] || die "daemon failed to start (omarchy-handoff log)"
  notify "Handoff daemon restarted"
}

# Show what the capture layer is actually doing -- the only way to tell a real
# barrier from a backend that selected cleanly and armed nothing.
cmd_log() {
  # The daemon runs as the lan-mouse.service user unit, so its output is in the
  # journal. It USED to be started by o.launch_on_start() in autostart.lua,
  # which sent stdout AND stderr to /dev/null -- so for hours this printed a
  # stale pre-reboot file while the live daemon logged nowhere at all. Read the
  # journal first; fall back to $LOG only for a hand-started daemon.
  if systemctl --user --quiet is-enabled lan-mouse.service 2>/dev/null \
     || systemctl --user --quiet is-active lan-mouse.service 2>/dev/null; then
    if [[ ${1:-} == -f ]]; then
      journalctl --user -u lan-mouse.service -o cat -f
    else
      journalctl --user -u lan-mouse.service -o cat --no-pager -n "${1:-40}"
    fi
    return
  fi
  [[ -f $LOG ]] || die "no journal unit and no log at $LOG (try: omarchy-handoff restart)"
  echo "# NOTE: reading fallback file $LOG (lan-mouse.service is not active)" >&2
  if [[ ${1:-} == -f ]]; then tail -f "$LOG"; else tail -n "${1:-40}" "$LOG"; fi
}

# Health check. Deliberately exits non-zero on failure: a check that can only
# report success is decoration. Scope is capture + trust-entry SHAPE -- it
# cannot prove the peer trusts our key, only that our own table isn't malformed.
# Pass a saved log as $1 to parse it instead of probing (used to test failures).
cmd_doctor() {
  local src="${1:-}" rc=0 probe cb last

  echo "== configured backend =="
  cb=$(python3 -I -c '
import sys, tomllib
try:
    with open(sys.argv[1], "rb") as f:
        print(tomllib.load(f).get("capture_backend") or "")
except Exception:
    print("")
' "$CONFIG")
  if [[ -z $cb ]]; then
    echo "  (unset -> defaults to input-capture-portal)"
    echo "  WARNING: that default is a SILENT NO-OP on Hyprland 0.56.2 -- it"
    echo "  initializes cleanly and creates no barriers. Set capture_backend = \"layer-shell\"."
    rc=1
  elif [[ $cb != layer-shell ]]; then
    echo "  $cb"
    echo "  WARNING: only layer-shell is known to create real barriers here."
    rc=1
  else
    echo "  layer-shell  (correct)"
  fi

  echo
  echo "== trust entry shape =="
  # Checks CASE as well as ordering. An uppercase key is a valid TOML table
  # entry and the right digest, so nothing anywhere complains -- the daemon
  # just never matches it and both ends send DTLS bad_certificate.
  local badcase
  badcase=$(python3 -I -c "
import sys, tomllib
try:
    with open(sys.argv[1], \"rb\") as f:
        fps = tomllib.load(f).get(\"authorized_fingerprints\", {})
except Exception:
    fps = {}
print(sum(1 for k in fps if any(c.isupper() for c in k)))
" "$CONFIG" 2>/dev/null || echo 0)
  if [[ ${badcase:-0} -gt 0 ]]; then
    echo "  FAILING -- ${badcase} fingerprint(s) contain UPPERCASE hex."
    echo "  lan-mouse compares the string verbatim and generates lowercase, so"
    echo "  an uppercase key never matches: the digest is right, the case is not,"
    echo "  nothing logs a complaint, and both ends send DTLS bad_certificate."
    echo "  Fix: omarchy-handoff fixcase"
    rc=1
  fi
  # Catches a REVERSED entry, which paired() cannot: a backwards
  # "description" = "<sha256>" line is a valid non-empty table, and auth then
  # fails silently and looks exactly like a dropped packet.
  python3 -I -c '
import sys, tomllib
try:
    with open(sys.argv[1], "rb") as f:
        fps = tomllib.load(f).get("authorized_fingerprints", {})
except Exception:
    print("  could not parse config"); sys.exit(1)
if not fps:
    print("  NO fingerprints authorized -- the peer cannot be trusted yet."); sys.exit(1)
bad = [k for k in fps if k.count(":") < 16]
for k, v in fps.items():
    label = "order OK " if k.count(":") >= 16 else "BACKWARDS"
    print(f"  {label} {k[:24]}... = {v!r}")
if bad:
    print("  ERROR: key must be the FINGERPRINT, value the description.")
    print("  Fix: lan-mouse cli remove-authorized-key <desc>, then")
    print("       lan-mouse cli authorize-key <desc> <sha256>")
    sys.exit(1)
' "$CONFIG" || rc=1

  echo
  echo "== peer reachability =="
  # Added after both machines spent hours green on doctor while a firewall ate
  # every inbound packet. Capture and trust can both be perfect and nothing
  # crosses. This reads the DAEMON'S OWN connect outcomes -- ground truth from
  # the real protocol, not a synthetic probe that proves nothing for UDP.
  local pip pport ufwline verdict
  pip=$(python3 -I -c "
import sys, tomllib
try:
    with open(sys.argv[1], \"rb\") as f:
        cs = tomllib.load(f).get(\"clients\", [])
    ips = cs[0].get(\"ips\", []) if cs else []
    print(ips[0] if ips else \"\")
except Exception:
    print(\"\")
" "$CONFIG" 2>/dev/null || true)
  pport=$(python3 -I -c "
import sys, tomllib
try:
    with open(sys.argv[1], \"rb\") as f:
        d = tomllib.load(f)
    print(d.get(\"clients\", [{}])[0].get(\"port\") or d.get(\"port\") or 4242)
except Exception:
    print(4242)
" "$CONFIG" 2>/dev/null || true)

  if [[ -z $pip ]]; then
    echo "  no peer ip configured -- nothing to reach."
    rc=1
  else
    echo "  peer: ${pip}:${pport}"
    if ping -c1 -W2 -q "$pip" >/dev/null 2>&1; then
      echo "  ICMP: host is up"
    else
      echo "  ICMP: no reply -- peer is off, asleep, or on another network."
      echo "  (Everything below will fail for that reason alone.)"
      rc=1
    fi

    # Our own inbound half. This is the check that was missing entirely.
    if command -v ufw >/dev/null && sudo -n ufw status >/dev/null 2>&1; then
      ufwline=$(sudo -n ufw status 2>/dev/null | grep -E "^${pport}/udp" || true)
      if [[ -n $ufwline ]]; then
        echo "  our ufw: ALLOWS ${pport}/udp -- ${ufwline}"
      else
        echo "  our ufw: NO rule for ${pport}/udp. If the default is deny incoming,"
        echo "  the peer's handshake never reaches our daemon and it looks like a"
        echo "  silent drop. Check: sudo ufw status verbose"
        rc=1
      fi
    else
      echo "  our ufw: not checked (needs passwordless sudo); verify by hand."
    fi

    # What did the real protocol actually do most recently?
    verdict=$(cmd_log 400 2>/dev/null | grep -aE "connect|Alert|connection" | tail -n1 || true)
    if [[ -z $verdict ]]; then
      echo "  daemon log: no connection attempt recorded yet."
      echo "  (Cross the edge once, then re-run -- the attempt is what tells us"
      echo "  whether this is a firewall, a trust, or a routing problem.)"
    else
      echo "  last connect line: ${verdict#*] }"
      case $verdict in
        *"Alert is Fatal or Close Notify"*)
          echo "  => the PEER ACTIVELY REJECTED our certificate (DTLS fatal alert)."
          echo "  Not a firewall: something answered. The peer does not trust"
          echo "  our fingerprint. Ours is:"
          echo "    $(cmd_fingerprint 2>/dev/null || echo "<no cert>")"
          echo "  The peer must authorize exactly that, keyed BY FINGERPRINT."
          rc=1 ;;
        *"Connection timed out"*)
          echo "  => NOTHING ANSWERED. Either the peer's firewall is dropping"
          echo "  ${pport}/udp, or our reply left via a different source address"
          echo "  than the peer sent to (two NICs on one subnet does this --"
          echo "  check: ip route get ${pip})."
          rc=1 ;;
        *"connection established"*|*"connected to"*)
          echo "  => transport is UP." ;;
      esac
    fi
  fi

  echo
  echo "== barrier probe =="
  if [[ -n $src ]]; then
    probe=$(cat "$src")
  else
    # test-capture runs until killed, so timeout's 124 is expected, not a failure.
    probe=$(timeout 8 lan-mouse --capture-backend "${cb:-layer-shell}" test-capture 2>&1 || true)
  fi

  if ! grep -q "adding capture" <<<"$probe"; then
    echo "  NO BARRIERS CREATED -- capture is dead."
    echo "  Full probe output:"
    sed 's/^/    /' <<<"$probe"
    rc=1
  else
    # test-capture probes ALL FOUR positions and the registration order is NOT
    # stable, so a bare `| tail -n1` returns whichever edge happened to land
    # last -- not ours. Observed this report "left" and "bottom" on consecutive
    # runs from an unchanged config. Validate only OUR configured edge.
    # Within that edge, the populated list grows as each output registers, so
    # the LAST line for it is the authoritative one.
    local pos
    pos=$(python3 -I -c '
import sys, tomllib
try:
    with open(sys.argv[1], "rb") as f:
        cs = tomllib.load(f).get("clients", [])
    print(cs[0].get("position", "") if cs else "")
except Exception:
    print("")
' "$CONFIG")
    if [[ -z $pos ]]; then
      echo "  cannot determine our configured edge from $CONFIG"
      rc=1
    else
      echo "  our configured edge: $pos"
      last=$(grep "adding capture for position ${pos} " <<<"$probe" | tail -n1 || true)
      if [[ -z $last ]]; then
        echo "  FAILING -- no barrier was ever created for \"$pos\"."
        echo "  (Other edges may have armed; that does not help us.)"
        rc=1
      else
        echo "  last line for $pos: ${last#*] }"
        if [[ $last == *"using outputs: []"* ]]; then
          echo "  FAILING -- the final pass for $pos lists no outputs."
          echo "  (An empty pass BEFORE populated ones is normal and transient.)"
          rc=1
        else
          echo "  OK -- barrier is real on the output(s) above, so $pos can fire."
        fi
      fi
    fi
  fi

  echo
  if (( rc == 0 )); then
    echo "RESULT: OK"
  else
    echo "RESULT: PROBLEMS FOUND (see above)"
  fi
  echo "  Scope: capture barriers, reachability, and the shape of OUR OWN"
  echo "  trust entry. It cannot prove the peer trusts our key unless the"
  echo "  daemon has actually attempted a connection -- cross the edge once."
  return $rc
}

case "${1:-status}" in
  status)      cmd_status ;;
  toggle)      shift; cmd_toggle "$@" ;;
  on|enable)   shift; cmd_on "$@" ;;
  off|disable) shift; cmd_off "$@" ;;
  position)    shift; cmd_position "$@" ;;
  peer)        shift; cmd_peer "$@" ;;
  fingerprint) cmd_fingerprint ;;
  pair)        shift; cmd_pair "$@" ;;
  restart)     cmd_restart ;;
  log)         shift; cmd_log "$@" ;;
  fixcase)     shift; cmd_fixcase "$@" ;;
  doctor)      shift; cmd_doctor "$@" ;;
  *) die "unknown command '${1}'" ;;
esac
