#!/usr/bin/env bash
set -euo pipefail

# QuickCall Trace — end-user installer
# Usage:
#   curl -fsSL https://quickcall.dev/trace/install.sh | bash -s -- <org> <api-key>
#   curl -fsSL https://quickcall.dev/trace/install.sh | bash -s -- --scoped <org> <api-key>
#
# Flags:
#   --scoped    Interactive repo/folder selection mode
#
# Always pushes to trace.quickcall.dev (override with QC_TRACE_INGEST_URL env var)
#
# Idempotent: safe to re-run. If already installed, prints version and exits.
# User-level only: no root, no sudo, no system-wide changes.

QC_TRACE_DIR="$HOME/.quickcall-trace"
LOCAL_INGEST_URL="http://localhost:19777/ingest"
REMOTE_INGEST_URL="https://trace.quickcall.dev/ingest"
ORG=""
API_KEY=""
SCOPED_MODE=false

# ─── Colors & Symbols ─────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'

# Vercel Skills–inspired symbols
S_ACTIVE='\033[0;32m◆\033[0m'    # green diamond — active step
S_DONE='\033[0;32m◇\033[0m'      # green outline diamond — completed step
S_CANCEL='\033[0;31m■\033[0m'    # red square — cancelled
S_SELECT='\033[0;32m●\033[0m'    # green filled — selected
S_UNSELECT='\033[2m○\033[0m'     # dim outline — unselected
S_BAR='\033[2m│\033[0m'           # dim bar — vertical guide
S_BAR_END='\033[2m└\033[0m'      # dim corner — last item
S_ARROW='\033[0;36m❯\033[0m'     # cyan arrow — pointer

step()  { echo -e "${S_ACTIVE} ${BOLD}$*${RESET}"; }
ok()    { echo -e "${S_DONE} $*"; }
warn()  { echo -e "${YELLOW}▲${RESET} $*"; }
fail()  { echo -e "${S_CANCEL} $*"; exit 1; }
bar()   { echo -e "${S_BAR}  $*"; }

# Parse arguments (named flags + positional shorthand)
while [ $# -gt 0 ]; do
    case "$1" in
        --scoped) SCOPED_MODE=true; shift ;;
        --org)   ORG="$2"; shift 2 ;;
        --org=*) ORG="${1#--org=}"; shift ;;
        --key)   API_KEY="$2"; shift 2 ;;
        --key=*) API_KEY="${1#--key=}"; shift ;;
        -*)      shift ;;
        *)
            if [ -z "$ORG" ]; then ORG="$1"
            elif [ -z "$API_KEY" ]; then API_KEY="$1"
            fi
            shift ;;
    esac
done

# Default: remote (trace.quickcall.dev). QC_TRACE_INGEST_URL env var wins if set.
if [ -n "${QC_TRACE_INGEST_URL:-}" ]; then
    INGEST_URL="$QC_TRACE_INGEST_URL"
else
    INGEST_URL="$REMOTE_INGEST_URL"
fi

if [ -z "$ORG" ]; then
    ORG="local"
fi

# ─── Repository Discovery Functions ──────────────────────────────────────────

# Fast git repo discovery using find (maxdepth 4 for speed)
find_git_repos() {
    { find ~/ -maxdepth 4 -type d -name ".git" 2>/dev/null || true; } | sed 's/\/\.git$//'
}

# Extract org name from a git remote URL
# git@github.com:org/repo.git → org
# https://github.com/org/repo.git → org
get_org_name() {
    local remote_url="$1"
    echo "$remote_url" | sed 's#:#/#g' | sed 's#.*/\([^/]*\)/[^/]*$#\1#'
}

# Interactive org-based repo selection — TUI with arrow keys
# ↑/↓ to navigate, Space to toggle, Enter to confirm
# Orgs with 2+ repos are auto-selected.
select_scoped_paths() {
    echo ""
    step "Scoped Installation"
    bar "${DIM}QuickCall will only monitor repos you select.${RESET}"
    bar ""
    bar "Scanning for git repositories..."

    # Fast find
    ALL_REPOS=$(find_git_repos)
    REPO_COUNT=$(echo "$ALL_REPOS" | grep -c . || echo "0")

    if [ "$REPO_COUNT" -eq 0 ]; then
        bar ""
        warn "No git repositories found."
        bar ""
        bar "Enter paths to monitor (comma-separated), or press Enter to skip:"
        read -rp "$(echo -e "${S_BAR}  ${S_ARROW} ")" manual_paths

        if [ -n "$manual_paths" ]; then
            IFS=',' read -ra SELECTED_PATHS <<< "$manual_paths"
            for i in "${!SELECTED_PATHS[@]}"; do
                path=$(echo "${SELECTED_PATHS[$i]}" | xargs)
                SELECTED_PATHS[$i]="${path/#\~/$HOME}"
            done
        else
            SELECTED_PATHS=()
        fi
        return
    fi

    # Build org → repos mapping
    # Each line: org_name|repo_basename|/full/repo/path
    local repo_data=""
    while IFS= read -r repo_path; do
        [ -z "$repo_path" ] && continue
        local remote_url org_name
        remote_url=$(git -C "$repo_path" remote get-url origin 2>/dev/null || echo "")
        if [ -n "$remote_url" ]; then
            org_name=$(get_org_name "$remote_url")
        else
            org_name=$(basename "$(dirname "$repo_path")")
        fi
        local repo_basename
        repo_basename=$(basename "$repo_path")
        repo_data="${repo_data}${org_name}|${repo_basename}|${repo_path}"$'\n'
    done <<< "$ALL_REPOS"

    # Get unique orgs sorted by repo count (desc)
    local org_list
    org_list=$(echo "$repo_data" | grep . | cut -d'|' -f1 | sort | uniq -c | sort -rn)
    local total_orgs
    total_orgs=$(echo "$org_list" | grep -c . || echo "0")

    bar "Found ${GREEN}${REPO_COUNT}${RESET} repos in ${GREEN}${total_orgs}${RESET} orgs"
    bar ""

    # Detect AI CLI tools present
    local tools_found=""
    [ -d "$HOME/.claude/projects" ] && tools_found="${tools_found} claude"
    [ -d "$HOME/.codex/sessions" ] && tools_found="${tools_found} codex"
    [ -d "$HOME/.gemini/tmp" ]     && tools_found="${tools_found} gemini"
    [ -d "$HOME/.cursor/projects" ] && tools_found="${tools_found} cursor"

    if [ -n "$tools_found" ]; then
        bar "${DIM}Detected AI tools:${RESET}${CYAN}${tools_found}${RESET}"
        bar ""
    fi

    # Build org data into temp files (bash 3.2 compatible)
    local idx=0
    local org_names=""       # newline-separated
    local org_counts=""      # newline-separated
    local org_selected=""    # newline-separated (1/0)

    while IFS= read -r line <&3; do
        [ -z "$line" ] && continue
        local count org
        count=$(echo "$line" | awk '{print $1}')
        org=$(echo "$line" | awk '{$1=""; print $0}' | xargs)

        org_names="${org_names}${org}"$'\n'
        org_counts="${org_counts}${count}"$'\n'

        # Auto-select orgs with 2+ repos
        if [ "$count" -ge 2 ]; then
            org_selected="${org_selected}1"$'\n'
        else
            org_selected="${org_selected}0"$'\n'
        fi

        # Collect repo names (max 6 shown)
        local repos_for_org repo_names_str="" shown=0 total=0
        repos_for_org=$(echo "$repo_data" | grep "^${org}|" | cut -d'|' -f2)
        total=$(echo "$repos_for_org" | grep -c . || echo "0")
        while IFS= read -r rname; do
            [ -z "$rname" ] && continue
            if [ "$shown" -lt 6 ]; then
                if [ -z "$repo_names_str" ]; then
                    repo_names_str="$rname"
                else
                    repo_names_str="${repo_names_str}, ${rname}"
                fi
            fi
            shown=$((shown + 1))
        done <<< "$repos_for_org"
        if [ "$total" -gt 6 ]; then
            repo_names_str="${repo_names_str}, +$((total - 6)) more"
        fi

        echo "$repo_names_str" > "/tmp/qc_org_repos_${idx}"
        echo "$repo_data" | grep "^${org}|" | cut -d'|' -f3 > "/tmp/qc_org_paths_${idx}"

        idx=$((idx + 1))
    done 3<<< "$org_list"

    # ── Non-interactive fallback (piped input / no TTY) ──────────────────────
    if [ ! -t 0 ]; then
        _select_scoped_noninteractive
        return
    fi

    # ── Interactive TUI — fixed-height viewport, single-buffer render ───────
    local cursor=0
    local scroll=0
    local max_visible=6
    local search_mode=false
    local search_query=""
    local filtered_indices=""  # newline-separated org indices matching search

    # Fewer orgs than viewport? Shrink.
    if [ "$total_orgs" -lt "$max_visible" ]; then
        max_visible=$total_orgs
    fi

    # Fixed line count: scroll_top(1) + visible(max_visible*2) + scroll_bot(1) + help(1) + search(1) = constant
    local fixed_lines=$(( max_visible * 2 + 4 ))

    # Build filtered index list from search query
    _build_filtered() {
        filtered_indices=""
        if [ -z "$search_query" ]; then
            # No filter — all orgs
            local i=0
            while [ "$i" -lt "$total_orgs" ]; do
                filtered_indices="${filtered_indices}${i}"$'\n'
                i=$((i + 1))
            done
        else
            local i=0
            local lc_query
            lc_query=$(echo "$search_query" | tr '[:upper:]' '[:lower:]')
            while [ "$i" -lt "$total_orgs" ]; do
                local org
                org=$(echo "$org_names" | sed -n "$((i + 1))p")
                local lc_org
                lc_org=$(echo "$org" | tr '[:upper:]' '[:lower:]')
                # Also search repo names
                local repos
                repos=$(cat "/tmp/qc_org_repos_${i}" 2>/dev/null | tr '[:upper:]' '[:lower:]')
                case "${lc_org} ${repos}" in
                    *"$lc_query"*) filtered_indices="${filtered_indices}${i}"$'\n' ;;
                esac
                i=$((i + 1))
            done
        fi
    }

    # Get filtered count (safe for set -e: always exits 0)
    _filtered_count() {
        if [ -z "$filtered_indices" ]; then
            echo "0"
        else
            local c
            c=$(printf '%s\n' "$filtered_indices" | grep -c . 2>/dev/null) || true
            echo "${c:-0}"
        fi
    }

    # Get real org index from filtered position (returns empty string if out of bounds)
    _filtered_idx() {
        local pos="$1"
        if [ -z "$filtered_indices" ]; then
            echo ""
            return
        fi
        printf '%s\n' "$filtered_indices" | sed -n "$((pos + 1))p"
    }

    # Read a single keypress from /dev/tty
    _read_key() {
        local key
        IFS= read -rsn1 key </dev/tty 2>/dev/null || true
        if [ "$key" = $'\x1b' ]; then
            # Consume entire escape sequence (arrow keys, option+key, etc.)
            local rest
            IFS= read -rsn5 -t 0.2 rest </dev/tty 2>/dev/null || true
            case "$rest" in
                '[A'*) echo "UP";;
                '[B'*) echo "DOWN";;
                *)     echo "ESC";;
            esac
        elif [ "$key" = $'\r' ] || [ "$key" = "" ]; then
            echo "ENTER"
        elif [ "$key" = " " ]; then
            echo "SPACE"
        elif [ "$key" = $'\x7f' ] || [ "$key" = $'\x08' ]; then
            echo "BACKSPACE"
        elif [ "$key" = $'\x03' ]; then
            # Ctrl+C — exit cleanly
            stty "$old_stty" </dev/tty 2>/dev/null || true
            tput cnorm 2>/dev/null || true
            echo ""
            exit 130
        else
            # Only pass through printable ASCII (space to tilde)
            case "$key" in
                [[:print:]]) echo "$key";;
                *)           echo "OTHER";;
            esac
        fi
    }

    # Build the entire frame (clear + content) into a buffer, write once
    _frame() {
        local buf=""
        local fcount
        fcount=$(_filtered_count)
        local vis=$max_visible
        if [ "$fcount" -lt "$vis" ]; then
            vis=$fcount
        fi

        # ── Build content lines into array ──
        # Search line
        if [ "$search_mode" = true ]; then
            buf+="${S_BAR}  ${CYAN}/${RESET} ${search_query}█"$'\n'
        else
            buf+="${S_BAR}  ${DIM}/ search${RESET}"$'\n'
        fi

        # Scroll hint top
        if [ "$scroll" -gt 0 ]; then
            buf+="${S_BAR}  ${DIM}↑ ${scroll} more${RESET}"$'\n'
        else
            buf+="${S_BAR}"$'\n'
        fi

        # Visible rows
        local shown=0
        local vi=$scroll
        while [ "$shown" -lt "$max_visible" ]; do
            if [ "$vi" -lt "$fcount" ]; then
                local real_idx
                real_idx=$(_filtered_idx "$vi")
                if [ -z "$real_idx" ]; then
                    buf+="${S_BAR}"$'\n'
                    buf+="${S_BAR}"$'\n'
                    shown=$((shown + 1))
                    vi=$((vi + 1))
                    continue
                fi
                local org count selected repo_display
                org=$(echo "$org_names" | sed -n "$((real_idx + 1))p")
                count=$(echo "$org_counts" | sed -n "$((real_idx + 1))p")
                selected=$(echo "$org_selected" | sed -n "$((real_idx + 1))p")
                repo_display=$(cat "/tmp/qc_org_repos_${real_idx}" 2>/dev/null)

                if [ "$vi" -eq "$cursor" ]; then
                    if [ "$selected" = "1" ]; then
                        buf+="${S_BAR}  ${S_ARROW} ${S_SELECT} ${BOLD}${org}${RESET}  ${DIM}(${count} repos)${RESET}"$'\n'
                    else
                        buf+="${S_BAR}  ${S_ARROW} ${S_UNSELECT} ${BOLD}${org}${RESET}  ${DIM}(${count} repos)${RESET}"$'\n'
                    fi
                else
                    if [ "$selected" = "1" ]; then
                        buf+="${S_BAR}    ${S_SELECT} ${org}  ${DIM}(${count} repos)${RESET}"$'\n'
                    else
                        buf+="${S_BAR}    ${S_UNSELECT} ${DIM}${org}  (${count} repos)${RESET}"$'\n'
                    fi
                fi
                buf+="${S_BAR}      ${DIM}${repo_display}${RESET}"$'\n'
            else
                # Empty padding rows to keep fixed height
                buf+="${S_BAR}"$'\n'
                buf+="${S_BAR}"$'\n'
            fi
            shown=$((shown + 1))
            vi=$((vi + 1))
        done

        # Scroll hint bottom
        local below=$(( fcount - scroll - max_visible ))
        if [ "$below" -gt 0 ]; then
            buf+="${S_BAR}  ${DIM}↓ ${below} more${RESET}"$'\n'
        else
            buf+="${S_BAR}"$'\n'
        fi

        # Help line — context-aware
        if [ "$search_mode" = true ]; then
            buf+="${S_BAR}  ${DIM}type${RESET} filter  ${DIM}↑↓${RESET} navigate  ${DIM}space${RESET} toggle  ${DIM}enter${RESET} done  ${DIM}esc${RESET} clear"$'\n'
        else
            buf+="${S_BAR}  ${DIM}j/k ↑↓${RESET} navigate  ${DIM}space${RESET} toggle  ${DIM}/${RESET} search  ${DIM}a${RESET} all  ${DIM}n${RESET} none  ${DIM}enter${RESET} confirm"$'\n'
        fi

        # ── Write: move up, overwrite each line, print new content ──
        # Move cursor up to start of our region
        printf '\033[%dA' "$fixed_lines"
        # Clear each line and move down
        local cl=0
        while [ "$cl" -lt "$fixed_lines" ]; do
            printf '\033[2K\n'
            cl=$((cl + 1))
        done
        # Move back up
        printf '\033[%dA' "$fixed_lines"
        # Write new content in one shot
        printf '%b' "$buf"
    }

    # Adjust scroll to keep cursor in viewport
    _adjust_scroll() {
        local fcount
        fcount=$(_filtered_count)
        if [ "$cursor" -lt "$scroll" ]; then
            scroll=$cursor
        elif [ "$cursor" -ge $((scroll + max_visible)) ]; then
            scroll=$((cursor - max_visible + 1))
        fi
        # Clamp
        local max_scroll=$((fcount - max_visible))
        if [ "$max_scroll" -lt 0 ]; then max_scroll=0; fi
        if [ "$scroll" -gt "$max_scroll" ]; then scroll=$max_scroll; fi
    }

    # Terminal setup: hide cursor, raw mode (prevents ^[[B leaking)
    local old_stty
    old_stty=$(stty -g </dev/tty 2>/dev/null)
    stty -icanon -echo </dev/tty 2>/dev/null || true
    tput civis 2>/dev/null || true
    trap 'stty "$old_stty" </dev/tty 2>/dev/null; tput cnorm 2>/dev/null || true' RETURN

    # Build initial filter (all orgs)
    _build_filtered

    # Initial render — just print, no clear needed yet
    # Print fixed_lines of content for the first frame
    {
        local buf=""
        local fcount
        fcount=$(_filtered_count)

        # Search line
        buf+="${S_BAR}  ${DIM}/ search${RESET}"$'\n'

        # Scroll hint top
        buf+="${S_BAR}"$'\n'

        # Visible rows
        local shown=0
        local vi=0
        while [ "$shown" -lt "$max_visible" ]; do
            if [ "$vi" -lt "$fcount" ]; then
                local real_idx
                real_idx=$(_filtered_idx "$vi")
                if [ -z "$real_idx" ]; then
                    buf+="${S_BAR}"$'\n'
                    buf+="${S_BAR}"$'\n'
                    shown=$((shown + 1))
                    vi=$((vi + 1))
                    continue
                fi
                local org count selected repo_display
                org=$(echo "$org_names" | sed -n "$((real_idx + 1))p")
                count=$(echo "$org_counts" | sed -n "$((real_idx + 1))p")
                selected=$(echo "$org_selected" | sed -n "$((real_idx + 1))p")
                repo_display=$(cat "/tmp/qc_org_repos_${real_idx}" 2>/dev/null)
                if [ "$vi" -eq 0 ]; then
                    if [ "$selected" = "1" ]; then
                        buf+="${S_BAR}  ${S_ARROW} ${S_SELECT} ${BOLD}${org}${RESET}  ${DIM}(${count} repos)${RESET}"$'\n'
                    else
                        buf+="${S_BAR}  ${S_ARROW} ${S_UNSELECT} ${BOLD}${org}${RESET}  ${DIM}(${count} repos)${RESET}"$'\n'
                    fi
                else
                    if [ "$selected" = "1" ]; then
                        buf+="${S_BAR}    ${S_SELECT} ${org}  ${DIM}(${count} repos)${RESET}"$'\n'
                    else
                        buf+="${S_BAR}    ${S_UNSELECT} ${DIM}${org}  (${count} repos)${RESET}"$'\n'
                    fi
                fi
                buf+="${S_BAR}      ${DIM}${repo_display}${RESET}"$'\n'
            else
                buf+="${S_BAR}"$'\n'
                buf+="${S_BAR}"$'\n'
            fi
            shown=$((shown + 1))
            vi=$((vi + 1))
        done

        local below=$(( fcount - max_visible ))
        if [ "$below" -gt 0 ]; then
            buf+="${S_BAR}  ${DIM}↓ ${below} more${RESET}"$'\n'
        else
            buf+="${S_BAR}"$'\n'
        fi
        buf+="${S_BAR}  ${DIM}↑↓${RESET} navigate  ${DIM}space${RESET} toggle  ${DIM}/${RESET} search  ${DIM}a${RESET} all  ${DIM}n${RESET} none  ${DIM}enter${RESET} confirm"$'\n'
        printf '%b' "$buf"
    }

    # Input loop
    while true; do
        local key
        key=$(_read_key)

        if [ "$search_mode" = true ]; then
            # Search mode key handling
            case "$key" in
                ESC)
                    search_mode=false
                    search_query=""
                    _build_filtered
                    cursor=0
                    scroll=0
                    read -rsn5 -t 0.1 </dev/tty 2>/dev/null || true
                    ;;
                ENTER)
                    # Exit search but keep filter active — flush any buffered keys
                    search_mode=false
                    read -rsn5 -t 0.1 </dev/tty 2>/dev/null || true
                    ;;
                BACKSPACE)
                    if [ -n "$search_query" ]; then
                        search_query="${search_query%?}"
                    else
                        search_mode=false
                    fi
                    _build_filtered
                    cursor=0
                    scroll=0
                    ;;
                UP)
                    local fcount
                    fcount=$(_filtered_count)
                    if [ "$cursor" -gt 0 ]; then
                        cursor=$((cursor - 1))
                    elif [ "$fcount" -gt 0 ]; then
                        cursor=$((fcount - 1))
                    fi
                    _adjust_scroll
                    ;;
                DOWN)
                    local fcount
                    fcount=$(_filtered_count)
                    if [ "$cursor" -lt $((fcount - 1)) ]; then
                        cursor=$((cursor + 1))
                    else
                        cursor=0
                    fi
                    _adjust_scroll
                    ;;
                SPACE)
                    local fcount
                    fcount=$(_filtered_count)
                    if [ "$fcount" -gt 0 ]; then
                        local real_idx
                        real_idx=$(_filtered_idx "$cursor")
                        if [ -n "$real_idx" ]; then
                            local current
                            current=$(echo "$org_selected" | sed -n "$((real_idx + 1))p")
                            if [ "$current" = "1" ]; then
                                org_selected=$(echo "$org_selected" | awk -v line="$((real_idx + 1))" 'NR==line{print "0";next}1')
                            else
                                org_selected=$(echo "$org_selected" | awk -v line="$((real_idx + 1))" 'NR==line{print "1";next}1')
                            fi
                        fi
                    fi
                    ;;
                OTHER)
                    ;;
                *)
                    # Printable character — append to search
                    search_query="${search_query}${key}"
                    _build_filtered
                    cursor=0
                    scroll=0
                    ;;
            esac
            _frame
            continue
        fi

        # Normal mode key handling
        case "$key" in
            UP|k)
                local fcount
                fcount=$(_filtered_count)
                if [ "$cursor" -gt 0 ]; then
                    cursor=$((cursor - 1))
                elif [ "$fcount" -gt 0 ]; then
                    cursor=$((fcount - 1))
                fi
                _adjust_scroll
                ;;
            DOWN|j)
                local fcount
                fcount=$(_filtered_count)
                if [ "$cursor" -lt $((fcount - 1)) ]; then
                    cursor=$((cursor + 1))
                else
                    cursor=0
                fi
                _adjust_scroll
                ;;
            SPACE)
                local fcount
                fcount=$(_filtered_count)
                if [ "$fcount" -gt 0 ]; then
                    local real_idx
                    real_idx=$(_filtered_idx "$cursor")
                    if [ -n "$real_idx" ]; then
                        local current
                        current=$(echo "$org_selected" | sed -n "$((real_idx + 1))p")
                        if [ "$current" = "1" ]; then
                            org_selected=$(echo "$org_selected" | awk -v line="$((real_idx + 1))" 'NR==line{print "0";next}1')
                        else
                            org_selected=$(echo "$org_selected" | awk -v line="$((real_idx + 1))" 'NR==line{print "1";next}1')
                        fi
                    fi
                fi
                ;;
            /)
                search_mode=true
                search_query=""
                ;;
            a|A)
                org_selected=""
                local i=0
                while [ "$i" -lt "$total_orgs" ]; do
                    org_selected="${org_selected}1"$'\n'
                    i=$((i + 1))
                done
                ;;
            n|N)
                org_selected=""
                local i=0
                while [ "$i" -lt "$total_orgs" ]; do
                    org_selected="${org_selected}0"$'\n'
                    i=$((i + 1))
                done
                ;;
            ENTER)
                break
                ;;
            *)
                continue
                ;;
        esac

        _frame
    done

    stty "$old_stty" </dev/tty 2>/dev/null || true
    tput cnorm 2>/dev/null || true
    echo ""

    # Build SELECTED_PATHS from selected orgs
    SELECTED_PATHS=()
    local i=0
    while [ "$i" -lt "$total_orgs" ]; do
        local selected
        selected=$(echo "$org_selected" | sed -n "$((i + 1))p")
        if [ "$selected" = "1" ]; then
            while IFS= read -r path; do
                [ -z "$path" ] && continue
                SELECTED_PATHS+=("$path")
            done < "/tmp/qc_org_paths_${i}"
        fi
        i=$((i + 1))
    done

    # Clean up temp files
    local c=0
    while [ "$c" -lt "$total_orgs" ]; do
        rm -f "/tmp/qc_org_repos_${c}" "/tmp/qc_org_paths_${c}"
        c=$((c + 1))
    done

    bar ""
    if [ ${#SELECTED_PATHS[@]} -eq 0 ]; then
        warn "No repos selected. QuickCall will monitor all sessions (global mode)."
    else
        local selected_org_count=0
        local i=0
        while [ "$i" -lt "$total_orgs" ]; do
            local sel
            sel=$(echo "$org_selected" | sed -n "$((i + 1))p")
            if [ "$sel" = "1" ]; then
                selected_org_count=$((selected_org_count + 1))
            fi
            i=$((i + 1))
        done

        bar "${GREEN}${selected_org_count} org(s), ${#SELECTED_PATHS[@]} repo(s) selected${RESET}"
        i=0
        local shown_idx=0
        while [ "$i" -lt "$total_orgs" ]; do
            local sel org count
            sel=$(echo "$org_selected" | sed -n "$((i + 1))p")
            if [ "$sel" = "1" ]; then
                org=$(echo "$org_names" | sed -n "$((i + 1))p")
                count=$(echo "$org_counts" | sed -n "$((i + 1))p")
                shown_idx=$((shown_idx + 1))
                if [ "$shown_idx" -eq "$selected_org_count" ]; then
                    echo -e "${S_BAR_END}  ${S_SELECT} ${org} ${DIM}(${count} repos)${RESET}"
                else
                    echo -e "${S_BAR}  ${S_SELECT} ${org} ${DIM}(${count} repos)${RESET}"
                fi
            fi
            i=$((i + 1))
        done
    fi
    echo ""
}

# Non-interactive fallback for piped input (tests/CI)
# Reads commands: numbers toggle orgs, "a"=all, "n"=none, ""=confirm
_select_scoped_noninteractive() {
    # Display orgs
    local i=0
    while [ "$i" -lt "$total_orgs" ]; do
        local org count selected repo_display
        org=$(echo "$org_names" | sed -n "$((i + 1))p")
        count=$(echo "$org_counts" | sed -n "$((i + 1))p")
        selected=$(echo "$org_selected" | sed -n "$((i + 1))p")
        repo_display=$(cat "/tmp/qc_org_repos_${i}" 2>/dev/null)
        bar ""
        if [ "$selected" = "1" ]; then
            echo -e "${S_BAR}  ${S_SELECT} $((i + 1)). ${BOLD}${org}${RESET}  ${DIM}(${count} repos)${RESET}"
        else
            echo -e "${S_BAR}  ${S_UNSELECT} $((i + 1)). ${BOLD}${org}${RESET}  ${DIM}(${count} repos)${RESET}"
        fi
        echo -e "${S_BAR}      ${DIM}${repo_display}${RESET}"
        i=$((i + 1))
    done

    # Read commands line by line
    while IFS= read -r input || [ -n "$input" ]; do
        # Empty = confirm
        if [ -z "$input" ]; then
            break
        fi
        case "$input" in
            a|A|all|ALL)
                org_selected=""
                i=0
                while [ "$i" -lt "$total_orgs" ]; do
                    org_selected="${org_selected}1"$'\n'
                    i=$((i + 1))
                done
                ;;
            n|N|none|NONE)
                org_selected=""
                i=0
                while [ "$i" -lt "$total_orgs" ]; do
                    org_selected="${org_selected}0"$'\n'
                    i=$((i + 1))
                done
                ;;
            m|M|manual)
                IFS= read -r manual_paths || true
                IFS=',' read -ra SELECTED_PATHS <<< "$manual_paths"
                for i in "${!SELECTED_PATHS[@]}"; do
                    local path
                    path=$(echo "${SELECTED_PATHS[$i]}" | xargs)
                    SELECTED_PATHS[$i]="${path/#\~/$HOME}"
                done
                local c=0
                while [ "$c" -lt "$total_orgs" ]; do
                    rm -f "/tmp/qc_org_repos_${c}" "/tmp/qc_org_paths_${c}"
                    c=$((c + 1))
                done
                return
                ;;
            *)
                # Toggle org numbers (space-separated)
                for num in $input; do
                    local idx=$((num - 1))
                    if [ "$idx" -ge 0 ] && [ "$idx" -lt "$total_orgs" ]; then
                        local current
                        current=$(echo "$org_selected" | sed -n "$((idx + 1))p")
                        if [ "$current" = "1" ]; then
                            org_selected=$(echo "$org_selected" | awk -v line="$((idx + 1))" 'NR==line{print "0";next}1')
                        else
                            org_selected=$(echo "$org_selected" | awk -v line="$((idx + 1))" 'NR==line{print "1";next}1')
                        fi
                    fi
                done
                ;;
        esac
    done

    # Build SELECTED_PATHS
    SELECTED_PATHS=()
    i=0
    while [ "$i" -lt "$total_orgs" ]; do
        local selected
        selected=$(echo "$org_selected" | sed -n "$((i + 1))p")
        if [ "$selected" = "1" ]; then
            while IFS= read -r path; do
                [ -z "$path" ] && continue
                SELECTED_PATHS+=("$path")
            done < "/tmp/qc_org_paths_${i}"
        fi
        i=$((i + 1))
    done

    # Clean up
    local c=0
    while [ "$c" -lt "$total_orgs" ]; do
        rm -f "/tmp/qc_org_repos_${c}" "/tmp/qc_org_paths_${c}"
        c=$((c + 1))
    done

    bar ""
    if [ ${#SELECTED_PATHS[@]} -eq 0 ]; then
        warn "No repos selected. QuickCall will monitor all sessions (global mode)."
    else
        local selected_org_count=0
        i=0
        while [ "$i" -lt "$total_orgs" ]; do
            local sel
            sel=$(echo "$org_selected" | sed -n "$((i + 1))p")
            if [ "$sel" = "1" ]; then
                selected_org_count=$((selected_org_count + 1))
            fi
            i=$((i + 1))
        done
        bar "${GREEN}${selected_org_count} org(s), ${#SELECTED_PATHS[@]} repo(s) selected${RESET}"
        i=0
        local shown_idx=0
        while [ "$i" -lt "$total_orgs" ]; do
            local sel org count
            sel=$(echo "$org_selected" | sed -n "$((i + 1))p")
            if [ "$sel" = "1" ]; then
                org=$(echo "$org_names" | sed -n "$((i + 1))p")
                count=$(echo "$org_counts" | sed -n "$((i + 1))p")
                shown_idx=$((shown_idx + 1))
                if [ "$shown_idx" -eq "$selected_org_count" ]; then
                    echo -e "${S_BAR_END}  ${S_SELECT} ${org} ${DIM}(${count} repos)${RESET}"
                else
                    echo -e "${S_BAR}  ${S_SELECT} ${org} ${DIM}(${count} repos)${RESET}"
                fi
            fi
            i=$((i + 1))
        done
    fi
    echo ""
}

# Write scoped configuration to config.json
write_scoped_config() {
    local config_file="$1"
    shift
    local paths=("$@")

    # Build JSON array of scoped paths
    local paths_json="["
    local first=true
    for path in "${paths[@]}"; do
        if [ "$first" = true ]; then
            first=false
        else
            paths_json+=","
        fi
        paths_json+="\"$path\""
    done
    paths_json+="]"

    # Create or update config with scoped_paths
    if [ -f "$config_file" ]; then
        python3 -c "
import json
with open('$config_file') as f: c = json.load(f)
c['org'] = '$ORG'
if '$API_KEY': c['api_key'] = '$API_KEY'
if '$INGEST_URL': c['ingest_url'] = '$INGEST_URL'
c['device_id'] = '$DEVICE_ID'
c['scoped_mode'] = True
c['scoped_paths'] = $paths_json
with open('$config_file', 'w') as f: json.dump(c, f, indent=2)
" 2>/dev/null || {
            echo "{
  \"org\": \"$ORG\",
  \"api_key\": \"$API_KEY\",
  \"ingest_url\": \"$INGEST_URL\",
  \"device_id\": \"$DEVICE_ID\",
  \"scoped_mode\": true,
  \"scoped_paths\": $paths_json
}" > "$config_file"
        }
    else
        echo "{
  \"org\": \"$ORG\",
  \"api_key\": \"$API_KEY\",
  \"ingest_url\": \"$INGEST_URL\",
  \"device_id\": \"$DEVICE_ID\",
  \"scoped_mode\": true,
  \"scoped_paths\": $paths_json
}" > "$config_file"
    fi
}

# ─── Step 1: Idempotency check ───────────────────────────────────────────────

echo ""
echo -e " ${BOLD}░█▀█░█░█░▀█▀░█▀▀░█░█░█▀▀░█▀█░█░░░█░░${RESET}"
echo -e " ${BOLD}░█░█░█░█░░█░░█░░░█▀▄░█░░░█▀█░█░░░█░░${RESET}"
echo -e " ${BOLD}░▀▀█░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀░▀░▀▀▀░▀▀▀${RESET}"
echo -e "  ${GREEN}trace${RESET}  ·  ai session collector"
echo ""

PID_FILE="$QC_TRACE_DIR/quickcall.pid"
if [ -f "$PID_FILE" ]; then
    PID=$(cat "$PID_FILE")
    if kill -0 "$PID" 2>/dev/null; then
        VERSION=$("$HOME/.local/bin/uvx" --quiet qc-trace@latest python -c "from qc_trace import __version__; print(__version__)" 2>/dev/null || echo "unknown")
        ok "QuickCall daemon already running (PID $PID, v$VERSION)"

        # If --scoped flag is present, allow reconfiguration
        if [ "$SCOPED_MODE" = true ]; then
            warn "Daemon is running. To reconfigure scoped paths, stop the daemon first:"
            echo "    quickcall stop"
            echo "Then re-run the installer with --scoped"
        fi
        exit 0
    fi
fi

# Also check if launchd/systemd service is already registered
OS="$(uname -s)"
EXISTING_SERVICE=false
if [ "$OS" = "Darwin" ]; then
    DOMAIN_TARGET="gui/$(id -u)"
    if launchctl print "$DOMAIN_TARGET/com.quickcall.traced" >/dev/null 2>&1; then
        if [ "$SCOPED_MODE" = true ]; then
            EXISTING_SERVICE=true
            ok "Service detected — will reconfigure after selection"
        else
            ok "QuickCall service already registered (launchd)"
            exit 0
        fi
    fi
elif [ "$OS" = "Linux" ]; then
    if systemctl --user is-active --quiet quickcall 2>/dev/null; then
        if [ "$SCOPED_MODE" = true ]; then
            EXISTING_SERVICE=true
            ok "Service detected — will reconfigure after selection"
        else
            ok "QuickCall service already running (systemd)"
            exit 0
        fi
    fi
fi

# ─── Step 2: Pre-flight checks ───────────────────────────────────────────────

# curl
if ! command -v curl >/dev/null 2>&1; then
    fail "curl is required but not found."
fi

# Warn if running as root
if [ "$(id -u)" = "0" ]; then
    warn "Running as root — the service will be installed for the root user."
fi

# ─── Step 3: Scoped Mode Selection ───────────────────────────────────────────

SELECTED_PATHS=()
if [ "$SCOPED_MODE" = true ]; then
    select_scoped_paths
fi

# ─── Step 4: Install uv (if not present) ─────────────────────────────────────

if command -v uv >/dev/null 2>&1; then
    ok "uv already installed ($(uv --version 2>/dev/null || echo 'unknown'))"
else
    step "Installing uv..."
    curl -LsSf https://astral.sh/uv/install.sh | sh
    export PATH="$HOME/.local/bin:$PATH"
    if command -v uv >/dev/null 2>&1; then
        ok "uv installed"
    else
        fail "uv installation failed"
    fi
fi

# Python 3.11+ (uv manages its own Python, so only check system python as fallback info)
if command -v python3 >/dev/null 2>&1; then
    PY_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
    ok "Python $PY_VERSION (uv will manage its own Python for quickcall)"
else
    ok "No system python3 found (uv will manage Python automatically)"
    PY_VERSION="managed-by-uv"
fi

# ─── Step 5: Configure shell ─────────────────────────────────────────────────

SHELL_CONFIG=""
if [ -f "$HOME/.zshrc" ]; then
    SHELL_CONFIG="$HOME/.zshrc"
elif [ -f "$HOME/.bashrc" ]; then
    SHELL_CONFIG="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
    SHELL_CONFIG="$HOME/.bash_profile"
fi

MARKER_BEGIN="# >>> quickcall-trace >>>"
MARKER_END="# <<< quickcall-trace <<<"

SHELL_BLOCK="$MARKER_BEGIN
export PATH=\"\$HOME/.local/bin:\$PATH\"
alias quickcall='uvx --quiet --no-cache --from qc-trace quickcall'
$MARKER_END"

if [ -n "$SHELL_CONFIG" ]; then
    if grep -q "$MARKER_BEGIN" "$SHELL_CONFIG" 2>/dev/null; then
        # Update existing block in-place
        # Delete from marker begin to marker end, then append new block
        LC_ALL=C sed -i.bak "/$MARKER_BEGIN/,/$MARKER_END/d" "$SHELL_CONFIG"
        rm -f "${SHELL_CONFIG}.bak"
    fi
    echo "$SHELL_BLOCK" >> "$SHELL_CONFIG"
    ok "Shell config updated ($SHELL_CONFIG)"
else
    warn "No shell config found (.zshrc/.bashrc) — add \$HOME/.local/bin to your PATH manually"
fi

# Ensure PATH is set for this session
export PATH="$HOME/.local/bin:$PATH"

# ─── Step 6: Verify quickcall alias works ────────────────────────────────────

# Remove any old uv tool install (replaced by alias)
uv tool uninstall qc-trace 2>/dev/null || true

# Clear stale cache to prevent corrupted venv issues
uv cache clean qc-trace --force >/dev/null 2>&1 || true

ok "quickcall alias configured (via uvx)"

# ─── Step 7: Create data directory + write org to config ─────────────────────

mkdir -p "$QC_TRACE_DIR"

# Generate a persistent device ID (survives re-installs, unique per user+machine)
DEVICE_ID_FILE="$QC_TRACE_DIR/.device_id"
if [ -f "$DEVICE_ID_FILE" ]; then
    DEVICE_ID=$(cat "$DEVICE_ID_FILE")
else
    DEVICE_ID=$(uv run python3 -c "import uuid; print(uuid.uuid4())")
    echo "$DEVICE_ID" > "$DEVICE_ID_FILE"
fi

CONFIG_FILE="$QC_TRACE_DIR/config.json"

if [ "$SCOPED_MODE" = true ] && [ ${#SELECTED_PATHS[@]} -gt 0 ]; then
    # Write scoped configuration
    write_scoped_config "$CONFIG_FILE" "${SELECTED_PATHS[@]}"
    ok "Scoped configuration saved"
else
    # Global mode (original behavior)
    if [ -f "$CONFIG_FILE" ]; then
        # Merge org + api_key into existing config (python one-liner, minimal deps)
        python3 -c "
import json, sys
with open('$CONFIG_FILE') as f: c = json.load(f)
c['org'] = '$ORG'
if '$API_KEY': c['api_key'] = '$API_KEY'
if '$INGEST_URL': c['ingest_url'] = '$INGEST_URL'
c['device_id'] = '$DEVICE_ID'
c['scoped_mode'] = False
c.pop('scoped_paths', None)
with open('$CONFIG_FILE', 'w') as f: json.dump(c, f, indent=2)
print()
" 2>/dev/null || echo "{\"org\": \"$ORG\", \"api_key\": \"$API_KEY\", \"ingest_url\": \"$INGEST_URL\", \"device_id\": \"$DEVICE_ID\", \"scoped_mode\": false}" > "$CONFIG_FILE"
    else
        echo "{\"org\": \"$ORG\", \"api_key\": \"$API_KEY\", \"ingest_url\": \"$INGEST_URL\", \"device_id\": \"$DEVICE_ID\", \"scoped_mode\": false}" > "$CONFIG_FILE"
    fi
fi

ok "Org set to: $ORG"
if [ -n "$API_KEY" ]; then
    ok "API key configured"
fi

# ─── Step 8: Generate run.sh wrapper with version check + crash loop ─────────

TRACE_SERVER="${INGEST_URL%/ingest}"
RUN_SCRIPT="$QC_TRACE_DIR/quickcall-daemon"

cat > "$RUN_SCRIPT" <<'RUNSH_EOF'
#!/bin/sh
UPDATE_CHECK_INTERVAL=${QC_UPDATE_INTERVAL:-300}  # check for updates (default 5 min)

get_latest_version() {
  # Try server first, fall back to PyPI
  V=$(curl -sf __TRACE_SERVER__/api/latest-version 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
  if [ -z "$V" ]; then
    V=$(curl -sf https://pypi.org/pypi/qc-trace/json 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
  fi
  echo "$V"
}

while true; do
  VERSION=$(get_latest_version)
  [ -z "$VERSION" ] && VERSION="latest"

  export PATH="$HOME/.local/bin:$PATH"

  # Kill any existing quickcall run processes to prevent duplicates
  pkill -f "quickcall run" 2>/dev/null || true
  sleep 1

  # Clear stale cache to prevent corrupted venv crashes
  uv cache clean qc-trace --force >/dev/null 2>&1 || true

  # Start daemon in background
  uvx --no-cache --from "qc-trace@${VERSION}" quickcall run &
  DAEMON_PID=$!
  echo "[quickcall] daemon started v${VERSION} (PID ${DAEMON_PID})"

  # Subloop: poll for updates while daemon is running
  while kill -0 "$DAEMON_PID" 2>/dev/null; do
    sleep "$UPDATE_CHECK_INTERVAL"
    NEW_VERSION=$(get_latest_version)
    [ -z "$NEW_VERSION" ] && continue
    if [ "$NEW_VERSION" != "$VERSION" ]; then
      echo "[quickcall] update available: v${VERSION} -> v${NEW_VERSION}, restarting..."
      kill "$DAEMON_PID" 2>/dev/null
      wait "$DAEMON_PID" 2>/dev/null
      break
    fi
  done

  # Wait for daemon to fully exit
  wait "$DAEMON_PID" 2>/dev/null

  # If the daemon exited (crash/update), wait briefly then restart
  sleep 5
done
RUNSH_EOF

sed -i.bak "s|__TRACE_SERVER__|${TRACE_SERVER}|g" "$RUN_SCRIPT"
rm -f "${RUN_SCRIPT}.bak"
chmod +x "$RUN_SCRIPT"

# Remove old run.sh if present
rm -f "$QC_TRACE_DIR/run.sh"

ok "Daemon wrapper written ($RUN_SCRIPT)"

# ─── Step 9: Install service (user-level, no root) ───────────────────────────

case "$OS" in
    Linux)
        step "Installing user-level systemd service..."
        SERVICE_DIR="$HOME/.config/systemd/user"
        SERVICE_FILE="$SERVICE_DIR/quickcall.service"
        mkdir -p "$SERVICE_DIR"
        cat > "$SERVICE_FILE" <<SYSTEMD_EOF
[Unit]
Description=QuickCall trace collection daemon
After=network-online.target

[Service]
Type=simple
Environment=QC_TRACE_INGEST_URL=$INGEST_URL
Environment=QC_TRACE_ORG=$ORG
Environment=QC_TRACE_API_KEY=$API_KEY
ExecStart=/bin/sh ${RUN_SCRIPT}
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
SYSTEMD_EOF

        if [ "$EXISTING_SERVICE" = true ]; then
            systemctl --user stop quickcall 2>/dev/null || true
        fi
        systemctl --user daemon-reload
        systemctl --user enable --now quickcall
        ok "systemd user service installed and started"
        echo "  Status:  systemctl --user status quickcall"
        echo "  Logs:    journalctl --user-unit quickcall -f"
        ;;

    Darwin)
        step "Installing launchd agent..."
        # ── Build minimal .app bundle so macOS shows "QuickCall" with a proper icon
        #    instead of "sh" with a Terminal icon in the background items notification
        APP_DIR="$QC_TRACE_DIR/QuickCall.app"
        APP_CONTENTS="$APP_DIR/Contents"
        APP_MACOS="$APP_CONTENTS/MacOS"
        APP_RESOURCES="$APP_CONTENTS/Resources"
        mkdir -p "$APP_MACOS" "$APP_RESOURCES"

        # App Info.plist — identifies the app to macOS
        cat > "$APP_CONTENTS/Info.plist" <<APPLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>com.quickcall.traced</string>
    <key>CFBundleName</key>
    <string>QuickCall</string>
    <key>CFBundleDisplayName</key>
    <string>QuickCall</string>
    <key>CFBundleExecutable</key>
    <string>quickcall-daemon</string>
    <key>CFBundleIconFile</key>
    <string>AppIcon</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>LSBackgroundOnly</key>
    <true/>
    <key>LSUIElement</key>
    <true/>
</dict>
</plist>
APPLIST_EOF

        # Symlink the daemon wrapper into the .app bundle
        ln -sf "$RUN_SCRIPT" "$APP_MACOS/quickcall-daemon"

        # Generate a simple app icon (blue circle with Q) using Python + macOS sips
        # Creates a 256x256 PNG, then converts to .icns
        python3 -c "
import struct, zlib, os

# Generate a 256x256 RGBA PNG with a blue circle and white Q
W = 256
pixels = bytearray()
for y in range(W):
    pixels.append(0)  # filter byte
    for x in range(W):
        # Distance from center
        cx, cy = W//2, W//2
        dx, dy = x - cx, y - cy
        dist = (dx*dx + dy*dy) ** 0.5
        r_outer = W//2 - 8
        if dist <= r_outer:
            # Blue circle (#2563EB)
            r, g, b, a = 37, 99, 235, 255
            # White Q letter (rough approximation)
            r_q_outer = W//2 - 60
            r_q_inner = W//2 - 90
            in_ring = r_q_inner <= dist <= r_q_outer
            # Q tail: diagonal line from center-right going down-right
            in_tail = (50 < dx < 85 and 20 < dy < 75 and abs(dx - dy - 30) < 18)
            if in_ring or in_tail:
                r, g, b = 255, 255, 255
        else:
            r, g, b, a = 0, 0, 0, 0
        pixels.extend([r, g, b, a])

def make_png(width, height, raw_data):
    def chunk(ctype, data):
        c = ctype + data
        return struct.pack('>I', len(data)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff)
    header = b'\\x89PNG\\r\\n\\x1a\\n'
    ihdr = chunk(b'IHDR', struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0))
    idat = chunk(b'IDAT', zlib.compress(bytes(raw_data), 9))
    iend = chunk(b'IEND', b'')
    return header + ihdr + idat + iend

png = make_png(W, W, pixels)
icon_path = os.path.expanduser('~/.quickcall-trace/QuickCall.app/Contents/Resources/AppIcon.png')
with open(icon_path, 'wb') as f:
    f.write(png)
" 2>/dev/null

        # Convert PNG to icns using macOS built-in tools
        ICONSET_DIR="$APP_RESOURCES/AppIcon.iconset"
        mkdir -p "$ICONSET_DIR"
        if [ -f "$APP_RESOURCES/AppIcon.png" ]; then
            # Create required icon sizes
            for size in 16 32 128 256; do
                sips -z $size $size "$APP_RESOURCES/AppIcon.png" --out "$ICONSET_DIR/icon_${size}x${size}.png" >/dev/null 2>&1
            done
            for size in 16 32 128; do
                double=$((size * 2))
                sips -z $double $double "$APP_RESOURCES/AppIcon.png" --out "$ICONSET_DIR/icon_${size}x${size}@2x.png" >/dev/null 2>&1
            done
            iconutil -c icns "$ICONSET_DIR" -o "$APP_RESOURCES/AppIcon.icns" 2>/dev/null
            rm -rf "$ICONSET_DIR" "$APP_RESOURCES/AppIcon.png"
        fi

        # Ad-hoc sign the app bundle so Gatekeeper doesn't complain
        codesign --force --deep -s - "$APP_DIR" 2>/dev/null || true
        ok "App bundle created ($APP_DIR)"

        # ── LaunchAgent plist — points to the .app bundle executable
        PLIST_DIR="$HOME/Library/LaunchAgents"
        PLIST_FILE="$PLIST_DIR/com.quickcall.traced.plist"
        mkdir -p "$PLIST_DIR"

        cat > "$PLIST_FILE" <<PLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.quickcall.traced</string>
    <key>Program</key>
    <string>${APP_MACOS}/quickcall-daemon</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>QC_TRACE_INGEST_URL</key>
        <string>${INGEST_URL}</string>
        <key>QC_TRACE_ORG</key>
        <string>${ORG}</string>
        <key>QC_TRACE_API_KEY</key>
        <string>${API_KEY}</string>
    </dict>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>${QC_TRACE_DIR}/quickcall.log</string>
    <key>StandardErrorPath</key>
    <string>${QC_TRACE_DIR}/quickcall.err</string>
</dict>
</plist>
PLIST_EOF

        DOMAIN_TARGET="gui/$(id -u)"
        if [ "$EXISTING_SERVICE" = true ]; then
            launchctl bootout "$DOMAIN_TARGET/com.quickcall.traced" 2>/dev/null || true
        fi
        launchctl bootstrap "$DOMAIN_TARGET" "$PLIST_FILE"

        ok "launchd agent installed and started"
        echo "  Plist:   $PLIST_FILE"
        echo "  Logs:    $QC_TRACE_DIR/quickcall.log"
        echo "  Errors:  $QC_TRACE_DIR/quickcall.err"
        ;;

    *)
        fail "Unsupported OS: $OS (supported: Linux, macOS)"
        ;;
esac

echo "  Data:    $QC_TRACE_DIR/"

# ─── Step 10: Verify ─────────────────────────────────────────────────────────

step "Verifying installation..."
sleep 2  # give daemon a moment to start

CURL_ARGS=(-s -o /dev/null -w "%{http_code}" -X POST "$INGEST_URL"
    -H "Content-Type: application/json")

if [ -n "$API_KEY" ]; then
    CURL_ARGS+=(-H "X-API-Key: $API_KEY")
fi

# Determine mode for heartbeat message
if [ "$SCOPED_MODE" = true ] && [ ${#SELECTED_PATHS[@]} -gt 0 ]; then
    INSTALL_MODE="scoped (${#SELECTED_PATHS[@]} paths)"
else
    INSTALL_MODE="global"
fi

HEARTBEAT=$(curl "${CURL_ARGS[@]}" \
    -d "[{
        \"id\": \"install-$(hostname)-$(date +%s)\",
        \"session_id\": \"install-verify\",
        \"source\": \"qc_trace_install\",
        \"msg_type\": \"system\",
        \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
        \"content\": \"install ok — $(whoami)@$(hostname) — $(uname -s) — python $PY_VERSION — mode: $INSTALL_MODE\",
        \"source_schema_version\": 1
    }]" 2>/dev/null || echo "000")

if [ "$HEARTBEAT" = "200" ] || [ "$HEARTBEAT" = "201" ]; then
    ok "Heartbeat sent to $INGEST_URL"
else
    warn "Could not reach ingest server ($INGEST_URL, HTTP $HEARTBEAT) — daemon will retry"
fi

echo ""
echo -e "${S_DONE} ${GREEN}${BOLD}QuickCall Trace installed successfully!${RESET}"
echo ""

if [ "$SCOPED_MODE" = true ] && [ ${#SELECTED_PATHS[@]} -gt 0 ]; then
    echo -e "${S_BAR}  ${BOLD}Mode${RESET}  Scoped Installation"
    echo -e "${S_BAR}  ${BOLD}Paths${RESET}"
    for path in "${SELECTED_PATHS[@]}"; do
        echo -e "${S_BAR}    ${S_SELECT} ${path/#$HOME/~}"
    done
else
    echo -e "${S_BAR}  ${BOLD}Mode${RESET}  Global Installation"
    echo -e "${S_BAR}  Monitoring all AI CLI sessions"
fi
echo -e "${S_BAR}"
echo -e "${S_BAR}  ${BOLD}Pushing to${RESET}  $INGEST_URL"
echo -e "${S_BAR}"
echo -e "${S_BAR}  ${DIM}Commands:${RESET}"
echo -e "${S_BAR}    quickcall status    ${DIM}# Check daemon + stats${RESET}"
echo -e "${S_BAR}    quickcall logs -f   ${DIM}# Follow daemon logs${RESET}"
echo -e "${S_BAR_END}    quickcall config    ${DIM}# View/edit configuration${RESET}"
echo ""
echo -e "  ${DIM}To activate in this shell: ${RESET}source $SHELL_CONFIG"
echo ""