#!/usr/bin/env bash
# pentest-statusline.sh — Claude Code statusline integration.
# Reads session JSON on stdin, writes a single styled line to stdout.
# Must stay well under 200ms (runs on every render).
#
# Env overrides:
#   NO_COLOR=1                    disable all ANSI styling
#   PENTEST_STATUSLINE_COLS=120   override auto-width detection
#   PENTEST_STATUSLINE=minimal    collapse to program + findings + ctx only

set +e

INPUT=$(cat)

# ---------------------------------------------------------------------------
# Dependency check: without jq we can't parse the stdin JSON. Emit one soft
# hint instead of silently producing an empty bar.
# ---------------------------------------------------------------------------
if ! command -v jq >/dev/null 2>&1; then
    printf "pentest-agents: [no jq — statusline disabled]"
    exit 0
fi

# ---------------------------------------------------------------------------
# Color primitives. Disabled when NO_COLOR is set (spec: no-color.org) or
# when stdout isn't a TTY and Claude Code hasn't requested colors. Claude
# Code itself renders the statusline so isatty isn't definitive — we trust
# NO_COLOR as the opt-out signal and default to colored output.
# ---------------------------------------------------------------------------
if [ -n "$NO_COLOR" ]; then
    R="" B="" D="" V=""
    FG_DIM="" FG_PROG="" FG_AGENT="" FG_FIND="" FG_CRIT="" FG_HIGH="" FG_MED=""
    FG_BRAIN="" FG_BRAIN_OK="" FG_BRAIN_X="" FG_CTX_OK="" FG_CTX_WARN="" FG_CTX_HOT=""
    FG_COST_OK="" FG_COST_WARN="" FG_COST_HOT="" FG_MODEL="" FG_TIME="" FG_HUNT=""
    FG_AUTO=""
else
    R=$'\e[0m';  B=$'\e[1m';  D=$'\e[2m';  V=$'\e[7m'
    FG_DIM=$'\e[38;5;244m'
    FG_PROG=$'\e[38;5;39m'       # blue
    FG_AGENT=$'\e[38;5;171m'     # magenta-pink
    FG_FIND=$'\e[38;5;252m'      # light grey (total count)
    FG_CRIT=$'\e[38;5;196m'      # red
    FG_HIGH=$'\e[38;5;208m'      # orange
    FG_MED=$'\e[38;5;220m'       # yellow
    FG_BRAIN=$'\e[38;5;117m'     # cyan
    FG_BRAIN_OK=$'\e[38;5;114m'  # green
    FG_BRAIN_X=$'\e[38;5;167m'   # muted red
    FG_CTX_OK=$'\e[38;5;114m'    # green
    FG_CTX_WARN=$'\e[38;5;220m'  # yellow
    FG_CTX_HOT=$'\e[48;5;196m\e[38;5;231m'  # white on red background
    FG_COST_OK=$'\e[38;5;114m'
    FG_COST_WARN=$'\e[38;5;220m'
    FG_COST_HOT=$'\e[38;5;196m'
    FG_MODEL=$'\e[38;5;245m'
    FG_TIME=$'\e[38;5;246m'
    FG_HUNT=$'\e[38;5;185m'      # muted yellow-green
    FG_AUTO=$'\e[38;5;213m'      # pink
fi


# ---------------------------------------------------------------------------
# Session / model / context extraction — single jq pass. Each jq invocation
# costs ~40ms so we batch every field we want from stdin into one call and
# split the result with shell.
#
# We join on SOH (\u0001) because TAB is a whitespace IFS char: bash's read
# collapses consecutive whitespace delimiters, which would merge empty
# fields and shift every subsequent value one slot to the left.
# ---------------------------------------------------------------------------
_jq_fields=$(printf '%s' "$INPUT" | jq -r '
    [
        .model.display_name            // "",
        .model.id                      // "",
        .workspace.current_dir         // "",
        (.context_window.used_percentage // "" | tostring),
        (.rate_limits.five_hour.used_percentage // "" | tostring),
        .agent.name                    // "",
        .session.id                    // "",
        .session.started_at            // "",
        (.context_window.total_input_tokens  // "" | tostring),
        (.context_window.total_output_tokens // "" | tostring)
    ] | join("\u0001")
' 2>/dev/null)
IFS=$'\001' read -r MODEL_RAW MODEL_ID CWD CTX_USED FIVE_HR AGENT_NAME SESSION_ID SESSION_STARTED TOTAL_IN TOTAL_OUT <<< "$_jq_fields"
[ -z "$CWD" ] && CWD=$(pwd)

# Short model family — sonnet/opus/haiku — for compact display.
MODEL_SHORT=""
if [ -n "$MODEL_RAW" ] || [ -n "$MODEL_ID" ]; then
    lower=$(printf '%s' "$MODEL_ID$MODEL_RAW" | tr '[:upper:]' '[:lower:]')
    case "$lower" in
        *opus*)    MODEL_SHORT="opus"   ;;
        *sonnet*)  MODEL_SHORT="sonnet" ;;
        *haiku*)   MODEL_SHORT="haiku"  ;;
        *)         MODEL_SHORT=$(printf '%s' "$MODEL_RAW" | cut -c1-10) ;;
    esac
fi

# ---------------------------------------------------------------------------
# Program / platform
# ---------------------------------------------------------------------------
PROGRAM=""
PLATFORM=""
if [ -f "$CWD/scope.yaml" ]; then
    PROGRAM=$(grep '^program:' "$CWD/scope.yaml" 2>/dev/null | head -1 \
        | sed 's/^program: *//;s/"//g;s/'"'"'//g' | cut -c1-25)
    PLATFORM=$(grep '^platform:' "$CWD/scope.yaml" 2>/dev/null | head -1 \
        | sed 's/^platform: *//' | tr -d ' ')
fi

# ---------------------------------------------------------------------------
# Findings: cache the slow jq pass (keyed on findings.json mtime) so we
# don't pay it on every render.
# ---------------------------------------------------------------------------
FINDINGS=0; CRITS=0; HIGHS=0; MEDS=0
if [ -f "$CWD/findings.json" ]; then
    mtime=$(stat -c %Y "$CWD/findings.json" 2>/dev/null || stat -f %m "$CWD/findings.json" 2>/dev/null)
    cache="/tmp/.pa-statusline-$(printf '%s' "$CWD" | md5sum 2>/dev/null | cut -d' ' -f1).findings"
    if [ -n "$mtime" ] && [ -f "$cache" ] && [ "$(head -1 "$cache" 2>/dev/null)" = "$mtime" ]; then
        IFS='|' read -r FINDINGS CRITS HIGHS MEDS < <(tail -1 "$cache")
    else
        read -r FINDINGS CRITS HIGHS MEDS < <(
            jq -r '
                (.findings | (if type=="object" then to_entries|map(.value) else . end)) as $f
                | ($f | length // 0) as $total
                | ($f | map(select((.severity // "")|ascii_downcase=="critical")) | length) as $c
                | ($f | map(select((.severity // "")|ascii_downcase=="high"))     | length) as $h
                | ($f | map(select((.severity // "")|ascii_downcase=="medium"))   | length) as $m
                | "\($total) \($c) \($h) \($m)"
            ' "$CWD/findings.json" 2>/dev/null
        )
        [ -z "$FINDINGS" ] && FINDINGS=0
        [ -z "$CRITS" ] && CRITS=0
        [ -z "$HIGHS" ] && HIGHS=0
        [ -z "$MEDS" ] && MEDS=0
        [ -n "$mtime" ] && printf '%s\n%s|%s|%s|%s\n' "$mtime" "$FINDINGS" "$CRITS" "$HIGHS" "$MEDS" > "$cache" 2>/dev/null
    fi
fi
if [ "$FINDINGS" = "0" ] && [ -f "$CWD/findings.md" ]; then
    FINDINGS=$(grep -c '^## Finding' "$CWD/findings.md" 2>/dev/null || echo 0)
fi

# ---------------------------------------------------------------------------
# Brain
# ---------------------------------------------------------------------------
BRAIN_DIR="$CWD/.claude/agent-memory-local/brain"
BRAIN_TARGETS=0; EXHAUSTED=0; EFFECTIVE=0
if [ -d "$BRAIN_DIR" ]; then
    [ -d "$BRAIN_DIR/targets" ] && BRAIN_TARGETS=$(find "$BRAIN_DIR/targets" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
    [ -f "$BRAIN_DIR/techniques/exhausted.md" ] && EXHAUSTED=$(grep -c '^\[' "$BRAIN_DIR/techniques/exhausted.md" 2>/dev/null || echo 0)
    [ -f "$BRAIN_DIR/techniques/effective.md" ] && EFFECTIVE=$(grep -c '^\[' "$BRAIN_DIR/techniques/effective.md" 2>/dev/null || echo 0)
fi

# ---------------------------------------------------------------------------
# Session stopwatch — prefer JSON session.started_at, fall back to a marker
# file keyed on session id, fall back to shell uptime.
# ---------------------------------------------------------------------------
SESS_DUR=0
NOW_EPOCH=$(date +%s)
if [ -n "$SESSION_STARTED" ]; then
    START_EPOCH=$(date -u -d "$SESSION_STARTED" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${SESSION_STARTED%Z}" +%s 2>/dev/null)
    [ -n "$START_EPOCH" ] && [ "$START_EPOCH" -gt 0 ] && SESS_DUR=$((NOW_EPOCH - START_EPOCH))
elif [ -n "$SESSION_ID" ]; then
    marker="/tmp/.pa-session-$SESSION_ID.start"
    if [ ! -f "$marker" ]; then
        echo "$NOW_EPOCH" > "$marker" 2>/dev/null
    fi
    START_EPOCH=$(cat "$marker" 2>/dev/null)
    [ -n "$START_EPOCH" ] && SESS_DUR=$((NOW_EPOCH - START_EPOCH))
fi
fmt_dur() {
    local s=$1
    [ "$s" -lt 60 ] && printf '%ss' "$s" && return
    [ "$s" -lt 3600 ] && printf '%dm' $((s / 60)) && return
    printf '%dh%02dm' $((s / 3600)) $(((s % 3600) / 60))
}

# ---------------------------------------------------------------------------
# Last hunt age + autopilot mode — one jq pass over the tail of the journal.
# Extracts every action/ts/details we care about as TSV; shell then picks
# the right rows.
# ---------------------------------------------------------------------------
LAST_ACT=""; LAST_AGE=""; AUTOPILOT_MODE=""
if [ -f "$CWD/.claude/autopilot.mode" ]; then
    AUTOPILOT_MODE=$(head -1 "$CWD/.claude/autopilot.mode" 2>/dev/null | tr -d '[:space:]' | cut -c1-10)
fi
if [ -f "$CWD/journal.jsonl" ]; then
    _recent=$(tail -100 "$CWD/journal.jsonl" 2>/dev/null | jq -r '
        select(.action | IN("hunt","autopilot","validate","chain","surface"))
        | [.action, .ts, (.details // "")] | join("\u0001")
    ' 2>/dev/null)
    if [ -n "$_recent" ]; then
        # Last action overall
        _last=$(printf '%s\n' "$_recent" | tail -1)
        IFS=$'\001' read -r LAST_ACT LAST_TS _last_det <<< "$_last"
        if [ -n "$LAST_TS" ]; then
            LAST_EPOCH=$(date -d "$LAST_TS" +%s 2>/dev/null)
            [ -n "$LAST_EPOCH" ] && LAST_AGE=$((NOW_EPOCH - LAST_EPOCH))
        fi
        # Autopilot mode: infer from the most recent autopilot line if within
        # the last 15 minutes and no explicit override file was found.
        if [ -z "$AUTOPILOT_MODE" ]; then
            _auto=$(printf '%s\n' "$_recent" | grep $'^autopilot\001' | tail -1)
            if [ -n "$_auto" ]; then
                IFS=$'\001' read -r _ AUTO_TS AUTO_DETAILS <<< "$_auto"
                AUTO_EPOCH=$(date -d "$AUTO_TS" +%s 2>/dev/null)
                if [ -n "$AUTO_EPOCH" ] && [ $((NOW_EPOCH - AUTO_EPOCH)) -lt 900 ]; then
                    det=$(printf '%s' "$AUTO_DETAILS" | tr '[:upper:]' '[:lower:]')
                    case "$det" in
                        *paranoid*) AUTOPILOT_MODE="paranoid" ;;
                        *normal*)   AUTOPILOT_MODE="normal"   ;;
                        *yolo*)     AUTOPILOT_MODE="yolo"     ;;
                        *)          AUTOPILOT_MODE="on"       ;;
                    esac
                fi
            fi
        fi
    fi
fi

# ---------------------------------------------------------------------------
# Segment rendering
# ---------------------------------------------------------------------------
SEG_PROGRAM=""
if [ -n "$PROGRAM" ]; then
    SEG_PROGRAM="${FG_PROG}🎯 ${B}${PROGRAM}${R}"
    [ -n "$PLATFORM" ] && SEG_PROGRAM+=" ${FG_DIM}(${PLATFORM})${R}"
else
    SEG_PROGRAM="${FG_DIM}🎯 ?${R}"
fi

SEG_MODEL=""
if [ -n "$MODEL_SHORT" ]; then
    SEG_MODEL="${FG_MODEL}⟨${MODEL_SHORT}⟩${R}"
fi

SEG_AGENT=""
if [ -n "$AGENT_NAME" ]; then
    SEG_AGENT="${FG_AGENT}🤖 ${AGENT_NAME}${R}"
fi

SEG_FINDINGS=""
if [ "$FINDINGS" -gt 0 ] 2>/dev/null; then
    SEG_FINDINGS="${FG_FIND}🔍 ${B}${FINDINGS}${R}"
    [ "$CRITS" -gt 0 ] 2>/dev/null && SEG_FINDINGS+=" ${FG_CRIT}●${CRITS}${R}"
    [ "$HIGHS" -gt 0 ] 2>/dev/null && SEG_FINDINGS+=" ${FG_HIGH}●${HIGHS}${R}"
    [ "$MEDS"  -gt 0 ] 2>/dev/null && SEG_FINDINGS+=" ${FG_MED}●${MEDS}${R}"
fi

SEG_BRAIN=""
if [ "$BRAIN_TARGETS" -gt 0 ] 2>/dev/null || [ "$EFFECTIVE" -gt 0 ] 2>/dev/null || [ "$EXHAUSTED" -gt 0 ] 2>/dev/null; then
    SEG_BRAIN="${FG_BRAIN}🧠 ${BRAIN_TARGETS}t${R} ${FG_BRAIN_OK}${EFFECTIVE}✓${R} ${FG_BRAIN_X}${EXHAUSTED}✗${R}"
fi

# Chain-pressure: count entries in chain-pending advisory (feeder findings
# waiting for chain dispatch). chain_pressure_hook.py writes this after
# every SubagentStop. Surfacing it pushes the hunter to /chain instead of
# drafting standalone feeder reports.
SEG_CHAIN=""
PENDING_FILE="$CWD/.claude/agent-memory-local/chain-pending.md"
if [ -f "$PENDING_FILE" ]; then
    PENDING=$(grep -c '^| `' "$PENDING_FILE" 2>/dev/null || echo 0)
    if [ "$PENDING" -gt 0 ] 2>/dev/null; then
        # Red for 3+, yellow for 1-2 (chain-pending is friction; clear it)
        if [ "$PENDING" -ge 3 ] 2>/dev/null; then
            SEG_CHAIN="${FG_CRIT}⛓ ${PENDING}${R}"
        else
            SEG_CHAIN="${FG_MED}⛓ ${PENDING}${R}"
        fi
    fi
fi

# Mini progress bar — `cells` filled blocks out of 12 total.
render_bar() {
    local pct=$1 total=${2:-12}
    local filled=$(( (pct * total + 50) / 100 ))
    [ "$filled" -gt "$total" ] && filled=$total
    [ "$filled" -lt 0 ] && filled=0
    local bar=""
    local i=0
    while [ $i -lt "$total" ]; do
        if [ $i -lt $filled ]; then bar+="▰"; else bar+="▱"; fi
        i=$((i+1))
    done
    printf '%s' "$bar"
}
SEG_CTX=""
if [ -n "$CTX_USED" ]; then
    CTX_INT=$(printf "%.0f" "$CTX_USED" 2>/dev/null || echo 0)
    bar=$(render_bar "$CTX_INT" 12)
    if [ "$CTX_INT" -ge 85 ] 2>/dev/null; then
        SEG_CTX="${FG_CTX_HOT} ctx ${bar} ${CTX_INT}% ${R}"
    elif [ "$CTX_INT" -ge 60 ] 2>/dev/null; then
        SEG_CTX="${FG_CTX_WARN}ctx ${bar} ${CTX_INT}%${R}"
    else
        SEG_CTX="${FG_CTX_OK}ctx ${bar} ${CTX_INT}%${R}"
    fi
fi

SEG_RATE=""
if [ -n "$FIVE_HR" ]; then
    FIVE_INT=$(printf "%.0f" "$FIVE_HR" 2>/dev/null || echo 0)
    bar=$(render_bar "$FIVE_INT" 12)
    SEG_RATE="${FG_DIM}5h ${bar} ${FIVE_INT}%${R}"
fi

SEG_COST=""
if [ -n "$TOTAL_IN" ] && [ -n "$TOTAL_OUT" ]; then
    COST=$(echo "$TOTAL_IN $TOTAL_OUT" | awk '{printf "%.2f", ($1 * 3 + $2 * 15) / 1000000}')
    if [ "$COST" != "0.00" ]; then
        cents=$(echo "$COST" | awk '{printf "%d", $1 * 100}')
        if [ "$cents" -ge 500 ] 2>/dev/null; then
            SEG_COST="${FG_COST_HOT}\$${COST}${R}"
        elif [ "$cents" -ge 100 ] 2>/dev/null; then
            SEG_COST="${FG_COST_WARN}\$${COST}${R}"
        else
            SEG_COST="${FG_COST_OK}\$${COST}${R}"
        fi
    fi
fi

SEG_STOPWATCH=""
if [ "$SESS_DUR" -gt 0 ]; then
    SEG_STOPWATCH="${FG_TIME}⏱ $(fmt_dur "$SESS_DUR")${R}"
fi

SEG_HUNT=""
if [ -n "$LAST_AGE" ]; then
    age_str=""
    [ "$LAST_AGE" -lt 60 ]   && age_str="${LAST_AGE}s"
    [ -z "$age_str" ] && [ "$LAST_AGE" -lt 3600 ]  && age_str="$((LAST_AGE / 60))m"
    [ -z "$age_str" ] && age_str="$((LAST_AGE / 3600))h"
    SEG_HUNT="${FG_HUNT}${LAST_ACT}:${age_str}${R}"
fi

SEG_AUTO=""
if [ -n "$AUTOPILOT_MODE" ]; then
    SEG_AUTO="${FG_AUTO}⚙ ${AUTOPILOT_MODE}${R}"
fi

# ---------------------------------------------------------------------------
# Compose — 3 semantic rows. Each row is independent; empty rows collapse
# so a fresh workspace doesn't show empty scaffolding.
#
#   Row 1 — identity & mode:  program · model · agent · autopilot · stopwatch
#   Row 2 — work state:       findings · brain · last-action
#   Row 3 — capacity:         context · 5h rate · cost
#
# Inside each row segments are joined by a dim middle-dot separator; rows
# are joined by newlines.
# ---------------------------------------------------------------------------
SEP="  ${FG_DIM}·${R}  "

render_row() {
    local out="" seg first=1
    for seg in "$@"; do
        [ -z "$seg" ] && continue
        if [ $first -eq 1 ]; then
            out="$seg"; first=0
        else
            out+="$SEP$seg"
        fi
    done
    printf '%s' "$out"
}

# PENTEST_STATUSLINE=minimal — single-row collapse for users who want the
# old one-line bar back.
if [ "$PENTEST_STATUSLINE" = "minimal" ]; then
    line=$(render_row "$SEG_PROGRAM" "$SEG_FINDINGS" "$SEG_CTX")
    printf '%s' "$line"
    exit 0
fi

ROW1=$(render_row "$SEG_PROGRAM" "$SEG_MODEL" "$SEG_AGENT" "$SEG_AUTO" "$SEG_STOPWATCH")
ROW2=$(render_row "$SEG_FINDINGS" "$SEG_CHAIN" "$SEG_BRAIN" "$SEG_HUNT")
ROW3=$(render_row "$SEG_CTX" "$SEG_RATE" "$SEG_COST")

out=""
for row in "$ROW1" "$ROW2" "$ROW3"; do
    [ -z "$row" ] && continue
    [ -n "$out" ] && out+=$'\n'
    out+="$row"
done
printf '%s' "$out"
