#!/usr/bin/env bash
# orch-up — complete the orch install on this machine.
#
# Idempotent. Safe to re-run any time. Does what `npm install -g @agent-ops/orch`
# can't do on its own:
#   1. Verifies runtime deps (tmux, jq) and reports missing
#   2. Runs the postinstall symlink farm (in case --ignore-scripts was used)
#   3. Merges the SessionStart hook entry into ~/.claude/settings.json (with backup)
#
# What it does NOT do:
#   - Install the npm package itself (that's `npm install -g @agent-ops/orch`)
#   - Install runtime deps (uses your package manager; prints the right command)
#   - Inject fleet doctrine into ~/.codex/AGENTS.md / ~/.gemini/GEMINI.md
#     (opinion you adopt deliberately; not bundled into 'up')
#
# Flags:
#   --dry-run    show what would change, do nothing
#   --quiet      suppress all output except errors
set -euo pipefail

QUIET=0
DRY_RUN=0
while [ $# -gt 0 ]; do
    case "$1" in
        --dry-run) DRY_RUN=1; shift ;;
        --quiet)   QUIET=1; shift ;;
        --help|-h) sed -n '2,21p' "$0"; exit 0 ;;
        *) echo "orch-up: unknown flag: $1" >&2; exit 1 ;;
    esac
done

say() { [ $QUIET -eq 0 ] && echo "$@" >&2 || true; }
warn() { echo "orch-up: WARN" "$@" >&2; }

# Locate package root from this binary's path.
BIN_PATH=$(perl -MCwd=realpath -le 'print realpath shift' "$0" 2>/dev/null || readlink -f "$0" 2>/dev/null || echo "$0")
PKG_ROOT=$(cd "$(dirname "$BIN_PATH")/.." && pwd)

say "=== orch-up — package: $PKG_ROOT ==="
[ $DRY_RUN -eq 1 ] && say "(dry-run mode — no changes will be made)"
say

# ───────────────────────────────────────────────────────────────────
# 1. Verify runtime deps
# ───────────────────────────────────────────────────────────────────
say "[1/4] checking runtime deps"
missing=()
for cmd in tmux jq; do
    if ! command -v "$cmd" >/dev/null 2>&1; then
        missing+=("$cmd")
    fi
done

if [ ${#missing[@]} -gt 0 ]; then
    case "$(uname -s)" in
        Darwin) hint="brew install ${missing[*]}" ;;
        Linux)
            if command -v apt >/dev/null 2>&1; then hint="sudo apt install ${missing[*]}"
            elif command -v dnf >/dev/null 2>&1; then hint="sudo dnf install ${missing[*]}"
            elif command -v pacman >/dev/null 2>&1; then hint="sudo pacman -S ${missing[*]}"
            else hint="install ${missing[*]} via your package manager"
            fi ;;
        *) hint="install ${missing[*]} via your platform package manager" ;;
    esac
    warn "missing: ${missing[*]}"
    warn "install with:  $hint"
    warn "re-run 'orch up' once deps are installed"
    exit 2
fi
say "  ok: tmux, jq all present"
say

# ───────────────────────────────────────────────────────────────────
# 2. Symlink farm (idempotent — postinstall handles this; here as fallback)
# ───────────────────────────────────────────────────────────────────
say "[2/4] symlinking hooks + skills into Claude Code tree"
if [ $DRY_RUN -eq 1 ]; then
    say "  would run: node $PKG_ROOT/scripts/postinstall.js"
elif [ -f "$PKG_ROOT/scripts/postinstall.js" ]; then
    if command -v node >/dev/null 2>&1; then
        node "$PKG_ROOT/scripts/postinstall.js" 2>&1 | sed 's/^/  /'
    else
        warn "node not on PATH; can't run postinstall.js. Install node and re-run."
        exit 2
    fi
else
    warn "scripts/postinstall.js not found at $PKG_ROOT/scripts/postinstall.js"
    warn "your install may be incomplete; reinstall via npm install -g @agent-ops/orch"
    exit 2
fi
say

# ───────────────────────────────────────────────────────────────────
# 3. Register hooks in ~/.claude/settings.json
# ───────────────────────────────────────────────────────────────────
say "[3/4] registering hooks in ~/.claude/settings.json"
SETTINGS="$HOME/.claude/settings.json"
SNIPPET="$PKG_ROOT/settings-snippet.json"

if [ ! -f "$SNIPPET" ]; then
    warn "settings-snippet.json not found at $SNIPPET"
    exit 2
fi

mkdir -p "$HOME/.claude"

# Post orch#94, the snippet ships a single SessionStart entry for the still-live
# orch-goal-session-context.sh hook. Stop/Notification wiring is retired — the
# Synadia Agent Protocol path via orch-agent-shim handles those events. We merge
# the SessionStart hook idempotently (detect duplicates by hook basename in the
# command string).

# Strip the _INSTRUCTIONS comment from snippet before merging.
SNIPPET_CLEAN=$(jq 'del(._INSTRUCTIONS)' "$SNIPPET")

# Expand $HOME in the snippet command strings — Claude Code doesn't expand vars.
SNIPPET_CLEAN=$(echo "$SNIPPET_CLEAN" | sed "s|\\\$HOME|$HOME|g")

merge_hook_for_event() {
    # Args: existing settings JSON (string), event name (e.g. SessionStart).
    # Returns merged settings on stdout. Detects orch's hook by extracting the
    # hook basename (e.g. orch-goal-session-context.sh) from the snippet's own
    # command string for this event.
    local existing=$1 event=$2 mark
    mark=$(echo "$SNIPPET_CLEAN" | jq -r --arg ev "$event" '.hooks[$ev][0].hooks[0].command' \
        | grep -oE 'orch-[a-zA-Z0-9-]+\.sh' | head -1)
    [ -n "$mark" ] || { warn "could not derive hook name for event $event from snippet"; return 1; }

    local already_present
    already_present=$(echo "$existing" | jq -r --arg ev "$event" --arg mark "$mark" \
        '[(.hooks // {})[$ev][]?.hooks[]?.command | select(. != null)] | any(test($mark))' 2>/dev/null || echo "false")

    if [ "$already_present" = "true" ]; then
        say "  $event: $mark already registered, skipping"
        echo "$existing"
        return
    fi

    say "  $event: appending $mark hook"
    local orch_hook
    orch_hook=$(echo "$SNIPPET_CLEAN" | jq --arg ev "$event" '.hooks[$ev][0].hooks[0]')

    # If event array exists with at least one matcher, append our hook to
    # the FIRST matcher's hooks array. Otherwise create a new event entry.
    echo "$existing" | jq \
        --arg ev "$event" \
        --argjson new "$orch_hook" \
        'if (.hooks // {})[$ev]? and ((.hooks[$ev] | length) > 0) then
            .hooks[$ev][0].hooks += [$new]
         else
            .hooks //= {} | .hooks[$ev] = [{"hooks": [$new]}]
         end'
}

if [ $DRY_RUN -eq 1 ]; then
    say "  would merge orch-goal-session-context.sh (SessionStart) into $SETTINGS"
else
    if [ -f "$SETTINGS" ]; then
        BACKUP="$SETTINGS.orch-bak.$(date +%Y%m%d-%H%M%S)"
        cp "$SETTINGS" "$BACKUP"
        say "  backup: $BACKUP"
        existing=$(cat "$SETTINGS")
    else
        say "  creating new $SETTINGS"
        existing='{}'
    fi

    merged=$(merge_hook_for_event "$existing" SessionStart)
    echo "$merged" | jq '.' > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
fi
say

# ───────────────────────────────────────────────────────────────────
# 4. Launch extensions (extensions/*/manifest.json with lifecycle=daemon)
#
# orch-up MUST stay oblivious to which extensions exist or what they do — it
# reads only the manifest's `name`, `lifecycle`, `trigger`, and `binary`
# fields. New extensions land by dropping a directory under extensions/;
# orch-up picks them up next time it runs. See extensions/README.md for the
# manifest contract.
# ───────────────────────────────────────────────────────────────────
say "[4/4] launching extensions"
EXTENSIONS_DIR="$PKG_ROOT/extensions"
if [ -d "$EXTENSIONS_DIR" ]; then
    shopt -s nullglob 2>/dev/null || true
    found_any=0
    for manifest in "$EXTENSIONS_DIR"/*/manifest.json; do
        found_any=1
        ext_dir=$(dirname "$manifest")
        name=$(jq -r '.name // ""' "$manifest")
        lifecycle=$(jq -r '.lifecycle // ""' "$manifest")
        trigger=$(jq -r '.trigger // ""' "$manifest")
        binary=$(jq -r '.binary // ""' "$manifest")

        if [ -z "$name" ] || [ -z "$binary" ]; then
            warn "  skipping $manifest (missing name or binary)"
            continue
        fi
        if [ "$lifecycle" != "daemon" ] || [ "$trigger" != "orch-up" ]; then
            say "  skipping $name (lifecycle=$lifecycle, trigger=$trigger)"
            continue
        fi

        bin_path="$ext_dir/$binary"
        # For Go binaries, the manifest points at a source dir. Resolution:
        #   1. vendor/<basename> from goreleaser archive (npm-installed users).
        #   2. .bin/<basename> lazy-built from source (dev checkout — Go
        #      source is present).
        #
        # Check vendor first unconditionally — npm-installed users have no
        # cmd/ source dir at all (it's stripped from the tarball), so a
        # "needs source dir + main.go" gate would skip them.
        base=$(basename "$bin_path")
        vendored="$PKG_ROOT/vendor/$base"
        if [ -x "$vendored" ]; then
            bin_path="$vendored"
        elif [ -d "$bin_path" ] && [ -f "$bin_path/main.go" ]; then
            built="$ext_dir/.bin/$base"
            needs_build=0
            if [ ! -x "$built" ] || [ "$bin_path/main.go" -nt "$built" ]; then
                needs_build=1
            fi
            if [ $needs_build -eq 1 ] && [ $DRY_RUN -eq 1 ]; then
                say "  would build: go build -o $built $bin_path"
                # Dry-run: skip launch step too (we'd be claiming success on a
                # binary we haven't built yet).
                continue
            fi
            if [ $needs_build -eq 1 ]; then
                if ! command -v go >/dev/null 2>&1; then
                    warn "  $name: no vendored binary at $vendored and 'go' not on PATH; cannot build $bin_path — skipping"
                    continue
                fi
                mkdir -p "$ext_dir/.bin"
                # Build using the absolute source dir as the package
                # path. `go build /abs/path` is portable since Go 1.11
                # (modules); it walks up to find go.mod. We cd into
                # PKG_ROOT first so the right module is selected.
                (cd "$PKG_ROOT" && go build -o "$built" "$bin_path") \
                    || { warn "  $name: build failed — skipping"; continue; }
            fi
            bin_path="$built"
        fi
        if [ ! -x "$bin_path" ]; then
            warn "  $name: $bin_path not executable — skipping"
            continue
        fi

        run_dir="$HOME/.orch/extensions/$name"
        pid_file="$run_dir/daemon.pid"
        log_file="$run_dir/daemon.log"

        # Don't double-launch: if pid file exists and process is alive, skip.
        if [ -f "$pid_file" ]; then
            old_pid=$(cat "$pid_file" 2>/dev/null || true)
            if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
                say "  $name: already running (pid=$old_pid)"
                continue
            fi
            rm -f "$pid_file"
        fi

        if [ $DRY_RUN -eq 1 ]; then
            say "  would launch: $bin_path (log → $log_file)"
            continue
        fi

        mkdir -p "$run_dir"
        nohup "$bin_path" >>"$log_file" 2>&1 < /dev/null &
        echo $! > "$pid_file"
        say "  launched $name (pid=$(cat "$pid_file"), log=$log_file)"
    done
    if [ $found_any -eq 0 ]; then
        say "  no extensions found under $EXTENSIONS_DIR"
    fi
else
    say "  no extensions/ directory — skipping"
fi
say

# ───────────────────────────────────────────────────────────────────
# Summary
# ───────────────────────────────────────────────────────────────────
say "orch is ready."
say
say "Verify with:  orch-version"
say "Tear down:    orch down"