#!/bin/bash
# ocsessnotify - post a message to the most recent opencode session.
#
# By default, discovers opencode servers on the LAN via mDNS and posts the
# message to the session with the most recent activity across all of them.
# Use --host/--port to target a specific server and --session-id to target a
# specific session instead of the most recent one.
#
# This tool only talks REST to opencode's HTTP API. It does no SSH, no
# terminal notifications - it posts a message to a session and exits.

VERSION=$(dpkg-query -W -f='${Version}' ocsessnotify 2>/dev/null)
[ -n "$VERSION" ] || VERSION=$(brew list --versions ocsessnotify 2>/dev/null | awk '{print $2}')
[ -n "$VERSION" ] || VERSION="unknown"

usage() {
  cat <<'EOF'
Usage: ocsessnotify [OPTIONS] MESSAGE

Post MESSAGE to the most recent opencode session on the LAN.

By default servers are discovered via mDNS and the session with the most
recent activity across all discovered servers is used.

Options:
  -h, --help          Show this help and exit
  -v, --version       Show version and exit
  -H, --host HOST     Server hostname/IP (default: 127.0.0.1)
  -p, --port PORT     Server port (default: 4096)
  -s, --session-id ID Post to this session ID instead of the most recent one
  -t, --timeout SECS  Per-request timeout in seconds (default: 3)

Examples:
  ocsessnotify "deploy complete"
  ocsessnotify --host macstudio.stack --port 4096 "deploy complete"
  ocsessnotify --session-id ses_1234 "specific session"
EOF
}

HOST=""
PORT=""
SESSION_ID=""
TIMEOUT=3
POST_TIMEOUT=1.5

while [ "$#" -gt 0 ]; do
  case "$1" in
    -h|--help)
      usage
      exit 0
      ;;
    -v|--version|version)
      echo "ocsessnotify $VERSION"
      exit 0
      ;;
    -H|--host)
      HOST="$2"
      shift 2
      ;;
    -p|--port)
      PORT="$2"
      shift 2
      ;;
    -s|--session-id)
      SESSION_ID="$2"
      shift 2
      ;;
    -t|--timeout)
      TIMEOUT="$2"
      shift 2
      ;;
    -*)
      echo "Error: unknown option '$1'" >&2
      usage >&2
      exit 1
      ;;
    *)
      break
      ;;
  esac
done

MESSAGE="$*"
if [ -z "$MESSAGE" ]; then
  echo "Error: no message given" >&2
  usage >&2
  exit 1
fi

command -v curl >/dev/null 2>&1 || { echo "Error: curl not found" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "Error: jq not found" >&2; exit 1; }

# discover_servers - emit "HOST<TAB>PORT" lines for opencode-* HTTP services.
# avahi-browse -p is CSV-ish: =;iface;proto;name;type;domain;host;address;port;txt
# dns-sd -Z is the macOS/Bonjour equivalent: PTR lines name the service, the
# matching SRV line carries the target hostname and port. mDNS hostnames such
# as "opencode.local." resolve natively via the system resolver.
discover_servers() {
  if command -v avahi-browse >/dev/null 2>&1; then
    avahi-browse -r -t -p _http._tcp 2>/dev/null | awk -F';' '
      $1 == "=" && $4 ~ /^opencode-/ && NF >= 9 {
        print $8 "\t" $9
      }' | sort -t$'\t' -k2,2 -k1,1 | awk -F'\t' '!seen[$2]++'
    return
  fi
  if command -v dns-sd >/dev/null 2>&1; then
    local tmp pid n
    tmp=$(mktemp)
    dns-sd -Z _http._tcp local. >"$tmp" 2>&1 & pid=$!
    n=0
    while kill -0 "$pid" 2>/dev/null && [ "$n" -lt 10 ] &&
        ! grep -qE '^opencode-[^[:space:]]+[[:space:]]+SRV[[:space:]]' "$tmp"; do
      sleep 0.1
      n=$((n + 1))
    done
    kill "$pid" 2>/dev/null
    wait "$pid" 2>/dev/null
    awk '
      $2 == "PTR" && $3 ~ /^opencode-/ { inst[$3] = 1 }
      $2 == "SRV" && $1 in inst { print $6 "\t" $5 }
    ' "$tmp" | sort -t$'\t' -k2,2 -k1,1 | awk -F'\t' '!seen[$2]++'
    rm -f "$tmp"
    return
  fi
  echo "Error: neither avahi-browse nor dns-sd found" >&2
  return 1
}

# most_recent_session HOST PORT - emit "UPDATED<TAB>ID<TAB>HOST<TAB>PORT" for
# the most recent session on the server. The /session endpoint is scoped to a
# single project (the server's own launch directory unless a directory is
# given), so finding the most recent session across every project requires a
# per-project scan. Each project is queried in parallel for its most recent
# session; the caller picks the newest by updated.
most_recent_session() {
  local host="$1" port="$2" projects cwd tmp
  projects=$(curl -s --max-time "$TIMEOUT" "http://${host}:${port}/project")
  cwd=$(curl -s --max-time "$TIMEOUT" "http://${host}:${port}/path" |
    jq -r '.directory // empty')
  tmp=$(mktemp)
  while IFS= read -r wt; do
    [ -n "$wt" ] || continue
    enc=$(printf '%s' "$wt" | jq -sRr '@uri')
    ( curl -s --max-time "$TIMEOUT" \
        "http://${host}:${port}/session?order=desc&limit=1&directory=${enc}" |
      jq -r --arg host "$host" --arg port "$port" \
        '.[0] | select(. != null) | "\(.time.updated)\t\(.id)\t\($host)\t\($port)"' ) \
      >>"$tmp" &
  done < <({
    echo "$projects" | jq -r '.[].worktree // empty'
    echo "$cwd"
  } | sort -u)
  wait
  cat "$tmp"
  rm -f "$tmp"
}

# session_exists HOST PORT ID - true if the server knows this session.
session_exists() {
  local host="$1" port="$2" id="$3"
  curl -s --max-time "$TIMEOUT" "http://${host}:${port}/session/${id}" |
    jq -e '.id != null' >/dev/null 2>&1
}

# post HOST PORT ID MESSAGE - POST the message to the session.
#
# The prompt_async endpoint records the message and returns 204 immediately;
# the session's agent generates the reply asynchronously, so there is no reply
# stream to wait on and no timeout. A 2xx response means delivered.
post() {
  local host="$1" port="$2" id="$3" message="$4" payload out rc
  payload=$(jq -cn --arg text "$message" \
    '{noReply:false,parts:[{type:"text",text:$text}]}')
  out=$(curl -s -o /dev/null -w '%{http_code}' --max-time "$POST_TIMEOUT" \
    -X POST "http://${host}:${port}/session/${id}/prompt_async" \
    -H "Content-Type: application/json" -d "$payload" 2>/dev/null)
  rc=$?
  if [ "$rc" -eq 0 ] && [ "${out#2}" != "$out" ]; then
    echo "Posted to session ${id} on ${host}:${port}"
    return 0
  fi
  echo "Error: POST to ${host}:${port} session ${id} failed (curl exit ${rc}, HTTP ${out})" >&2
  return 1
}

TARGET_HOST="${HOST:-127.0.0.1}"
TARGET_PORT="${PORT:-4096}"

if [ -z "$HOST" ] && [ -z "$PORT" ]; then
  # mDNS discovery mode
  SERVERS=$(discover_servers) || exit 1
  if [ -z "$SERVERS" ]; then
    echo "Error: no opencode servers discovered via mDNS" >&2
    exit 1
  fi
  if [ -n "$SESSION_ID" ]; then
    # Find which discovered server hosts the requested session.
    while IFS=$'\t' read -r sh sp; do
      [ -n "$sh" ] || continue
      if session_exists "$sh" "$sp" "$SESSION_ID"; then
        TARGET_HOST="$sh"
        TARGET_PORT="$sp"
        FOUND=1
        break
      fi
    done <<<"$SERVERS"
    if [ -z "${FOUND:-}" ]; then
      echo "Error: session ${SESSION_ID} not found on any discovered server" >&2
      exit 1
    fi
  else
    # Most recent session across all discovered servers.
    BEST=""
    while IFS=$'\t' read -r sh sp; do
      [ -n "$sh" ] || continue
      BEST="${BEST}
$(most_recent_session "$sh" "$sp")"
    done <<<"$SERVERS"
    BEST=$(printf '%s\n' "$BEST" | sed '/^[[:space:]]*$/d' |
      sort -t$'\t' -k1,1 -rn | head -1)
    if [ -z "$BEST" ]; then
      echo "Error: no sessions found on any discovered server" >&2
      exit 1
    fi
    TARGET_HOST=$(printf '%s\n' "$BEST" | cut -f3)
    TARGET_PORT=$(printf '%s\n' "$BEST" | cut -f4)
    SESSION_ID=$(printf '%s\n' "$BEST" | cut -f2)
  fi
else
  # Explicit host/port: session must be given or derived from that server.
  if [ -z "$SESSION_ID" ]; then
    BEST=$(most_recent_session "$TARGET_HOST" "$TARGET_PORT" |
      sort -t$'\t' -k1,1 -rn | head -1)
    if [ -z "$BEST" ]; then
      echo "Error: no sessions found on ${TARGET_HOST}:${TARGET_PORT}" >&2
      exit 1
    fi
    SESSION_ID=$(printf '%s\n' "$BEST" | cut -f2)
  fi
fi

post "$TARGET_HOST" "$TARGET_PORT" "$SESSION_ID" "$MESSAGE"
