#!/bin/bash
# Overwatch Universal Hook Adapter
# This script handles communication between AI tools and the Overwatch daemon.
# Usage: ./universal-hook.sh <source> <event> <default_json>

set -euo pipefail

IDE_SOURCE=$1
HOOK_EVENT=$2
DEFAULT_RESPONSE=${3:-"{}"}

# Auto-start daemon in background if not running (fire-and-forget)
try_auto_start() {
    local LOCK_DIR="$HOME/.overwatch/daemon.lock.d"
    if command -v overwatch >/dev/null 2>&1; then
        # Use mkdir as an atomic lock — only one process can succeed
        if mkdir "$LOCK_DIR" 2>/dev/null; then
            nohup overwatch start --hook >/dev/null 2>&1 &
        else
            # Lock exists — remove if stale (>30s). GNU stat (-c) before BSD (-f): on GNU, -f is filesystem mode and poisons the arithmetic (#699).
            local LOCK_MTIME
            LOCK_MTIME=$(stat -c %Y "$LOCK_DIR" 2>/dev/null || stat -f %m "$LOCK_DIR" 2>/dev/null || echo 0)
            # Non-numeric stat output falls back to 0 (reads as stale) instead of crashing under set -e.
            case $LOCK_MTIME in (''|*[!0-9]*) LOCK_MTIME=0 ;; esac
            local LOCK_AGE=$(( $(date +%s) - LOCK_MTIME ))
            if [ "$LOCK_AGE" -gt 30 ]; then
                rmdir "$LOCK_DIR" 2>/dev/null || true
            fi
        fi
    fi
}

# Read the Overwatch daemon port from the discovery file (falling back to the
# legacy name written by pre-rename builds), default to static port 17580. An
# empty/torn file makes `cat` succeed with no output, so the `|| cat` chain
# would not fall through; guard each step with [ -z ] explicitly. `|| true`
# keeps a missing file from tripping `set -e` on the assignment.
OVERWATCH_PORT=$(cat "$HOME/.overwatch/overwatch_port" 2>/dev/null || true)
[ -z "$OVERWATCH_PORT" ] && OVERWATCH_PORT=$(cat "$HOME/.overwatch/guardian_port" 2>/dev/null || true)
[ -z "$OVERWATCH_PORT" ] && OVERWATCH_PORT="17580"
ENDPOINT="http://127.0.0.1:${OVERWATCH_PORT}/hook/${IDE_SOURCE}/${HOOK_EVENT}"

# 2. Read payload from stdin (needed for dedup check and forwarding)
PAYLOAD=$(cat)

# 3. Cross-IDE dedup: skip if Cursor is firing a non-cursor hook.
#    Cursor picks up hooks from other IDEs (e.g. ~/.claude/settings.json) and fires them,
#    causing duplicate processing. Detect this via cursor_version in the payload.
if [ "$IDE_SOURCE" != "cursor" ] && echo "$PAYLOAD" | grep -q '"cursor_version"'; then
    echo "{}"
    exit 0
fi

# 4. Health Check & Forward Payload
# We use a 1s timeout for the health check to avoid hanging if the daemon is dead.
if curl -s -f -m 1 "http://127.0.0.1:${OVERWATCH_PORT}/health" >/dev/null 2>&1; then
    # Forward payload to daemon and capture response. The X-Overwatch-Default-Response
    # header carries $3 (the per-IDE deny envelope) so the daemon can echo it
    # for GENUINE denies (policy / step-up) instead of reconstructing IDE shapes
    # (ramparts#671). 25s timeout: must exceed the daemon's step-up resolve
    # budget (stepUpHookBudgetMs, default 20s). If the POST itself fails (daemon
    # died mid-request, network), fail OPEN with an empty allow, not $3.
    RESPONSE=$(echo "$PAYLOAD" | curl -s -m 25 -X POST "$ENDPOINT" \
        -H "Content-Type: application/json" \
        -H "X-Overwatch-Default-Response: $DEFAULT_RESPONSE" \
        -d @- 2>/dev/null || echo "{}")
else
    # Daemon is unreachable — try to auto-start it and fail OPEN (allow) for
    # this invocation so the developer is not blocked while it spins up.
    try_auto_start
    RESPONSE="{}"
fi

# 5. Pass-through: every supported agent reads stdout JSON.
echo "$RESPONSE"
