#!/usr/bin/env bash
# Check if all phases in task_plan.md are complete
# Default invocation: advisory echo, always exits 0 (Stop hook status report).
# With --gate: deliberate completion gate, opt-in per plan via <plan-dir>/.mode.
# Used by Stop hook to report task completion status.
#
# Plan-file resolution (v2.40+):
#   1. $1 (explicit path) — first non-flag positional argument
#   2. resolve-plan-dir.sh: $PLAN_ID env → .planning/.active_plan → newest mtime
#   3. Legacy ./task_plan.md
#
# This restores slug-mode parity: the Stop hook and any caller invoking with
# zero args now respects the active plan dir instead of silently defaulting to
# the legacy root path.
#
# Gate mode (v3, --gate flag):
#   The gate is OFF unless ALL of these hold (design "Gate decision table"):
#     1. <plan-dir>/.mode exists and contains "gate" (explicit opt-in)
#     2. an in_progress phase exists (not merely complete<total)
#     3. the Stop hook input JSON on stdin does not set stop_hook_active=true
#     4. the block counter (<plan-dir>/.stop_blocks) is below cap (PWF_GATE_CAP, default 20)
#     5. the ledger advanced since the last block (stall → allow stop)
#   When all hold, it emits a single-line block-decision JSON on stdout and
#   exits 0. Otherwise it falls back to advisory output and exits 0.
#   Without --gate, or in non-gated mode, behavior is byte-equivalent to v2.43.
#
# Stdin handling: the Claude Code Stop hook pipes a JSON payload on stdin. To
# avoid hanging when nothing is piped, stdin is read ONLY when fd 0 is not a
# TTY ([ -t 0 ]). Hook-piped input is EOF-terminated, so the read returns; an
# interactive terminal (TTY) is skipped entirely. No data on stdin is treated
# as stop_hook_active=false.

GATE=0
PLAN_FILE=""
for _arg in "$@"; do
    case "$_arg" in
        --gate) GATE=1 ;;
        *)
            if [ -z "$PLAN_FILE" ]; then
                PLAN_FILE="$_arg"
            fi
            ;;
    esac
done

PLAN_DIR=""
if [ -n "${PLAN_FILE}" ]; then
    PLAN_DIR="$(dirname "${PLAN_FILE}")"
else
    SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd 2>/dev/null)" || SCRIPT_DIR="."
    RESOLVER="${SCRIPT_DIR}/resolve-plan-dir.sh"
    RESOLVED_DIR=""
    if [ -f "${RESOLVER}" ]; then
        RESOLVED_DIR="$(sh "${RESOLVER}" 2>/dev/null)"
    fi
    if [ -n "${RESOLVED_DIR}" ] && [ -f "${RESOLVED_DIR}/task_plan.md" ]; then
        PLAN_FILE="${RESOLVED_DIR}/task_plan.md"
        PLAN_DIR="${RESOLVED_DIR}"
    else
        PLAN_FILE="task_plan.md"
        PLAN_DIR="."
    fi
fi

if [ ! -f "$PLAN_FILE" ]; then
    echo "[planning-with-files] No task_plan.md found — no active planning session."
    exit 0
fi

# Count total phases
TOTAL=$(grep -c "### Phase" "$PLAN_FILE" || true)

# Count both formats per field and keep the larger of the two. A plan may mix
# '**Status:** pending' on one phase with '[in_progress]' on another; counting
# only the primary format (and falling back to inline ONLY when all three
# primaries are zero) lost the inline count and let an in_progress plan slip
# past the gate. Per-field max preserves the legacy single-format result
# (the other format contributes 0) while catching mixed plans.
COMPLETE_PRIMARY=$(grep -cF "**Status:** complete" "$PLAN_FILE" || true)
IN_PROGRESS_PRIMARY=$(grep -cF "**Status:** in_progress" "$PLAN_FILE" || true)
PENDING_PRIMARY=$(grep -cF "**Status:** pending" "$PLAN_FILE" || true)

COMPLETE_INLINE=$(grep -c "\[complete\]" "$PLAN_FILE" || true)
IN_PROGRESS_INLINE=$(grep -c "\[in_progress\]" "$PLAN_FILE" || true)
PENDING_INLINE=$(grep -c "\[pending\]" "$PLAN_FILE" || true)

: "${COMPLETE_PRIMARY:=0}"; : "${IN_PROGRESS_PRIMARY:=0}"; : "${PENDING_PRIMARY:=0}"
: "${COMPLETE_INLINE:=0}";  : "${IN_PROGRESS_INLINE:=0}";  : "${PENDING_INLINE:=0}"

if [ "$COMPLETE_INLINE" -gt "$COMPLETE_PRIMARY" ]; then COMPLETE="$COMPLETE_INLINE"; else COMPLETE="$COMPLETE_PRIMARY"; fi
if [ "$IN_PROGRESS_INLINE" -gt "$IN_PROGRESS_PRIMARY" ]; then IN_PROGRESS="$IN_PROGRESS_INLINE"; else IN_PROGRESS="$IN_PROGRESS_PRIMARY"; fi
if [ "$PENDING_INLINE" -gt "$PENDING_PRIMARY" ]; then PENDING="$PENDING_INLINE"; else PENDING="$PENDING_PRIMARY"; fi

# Default to 0 if empty
: "${TOTAL:=0}"
: "${COMPLETE:=0}"
: "${IN_PROGRESS:=0}"
: "${PENDING:=0}"

# advisory_report: the v2.43 status echo. Always exit 0 after calling.
advisory_report() {
    if [ "$COMPLETE" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
        echo "[planning-with-files] ALL PHASES COMPLETE ($COMPLETE/$TOTAL). If the user has additional work, add new phases to task_plan.md before starting."
    else
        echo "[planning-with-files] Task in progress ($COMPLETE/$TOTAL phases complete). Update progress.md before stopping."
        if [ "$IN_PROGRESS" -gt 0 ]; then
            echo "[planning-with-files] $IN_PROGRESS phase(s) still in progress."
        fi
        if [ "$PENDING" -gt 0 ]; then
            echo "[planning-with-files] $PENDING phase(s) pending."
        fi
    fi
}

# ---- Default (advisory) path: byte-equivalent to v2.43 ----
if [ "$GATE" -ne 1 ]; then
    advisory_report
    exit 0
fi

# ---- Gate path (--gate). Resolves to advisory unless every guard says block. ----

# Guard 1: gated mode. The .mode file must contain "gate". Absent or other
# content means advisory mode (legacy behavior preserved).
MODE_FILE="${PLAN_DIR}/.mode"
if [ ! -f "${MODE_FILE}" ] || ! grep -q "gate" "${MODE_FILE}" 2>/dev/null; then
    advisory_report
    exit 0
fi

# Guard 3: stop_hook_active. Read the Stop hook JSON from stdin only when fd 0
# is not a TTY (see header). A true value means we are already inside a forced
# continuation; allow the stop to avoid runaway recursion.
STDIN_JSON=""
if [ ! -t 0 ]; then
    STDIN_JSON="$(cat 2>/dev/null)"
fi
# Anchor on the VALUE: "stop_hook_active" immediately followed (allowing
# whitespace and the colon) by true. A bare glob like *stop_hook_active*true*
# false-positives on '{"stop_hook_active": false, "other": true}', which would
# silently disable the gate. Newlines are collapsed so the match works whether
# the payload is pretty-printed or single-line.
STOP_HOOK_ACTIVE="$(
    printf '%s' "${STDIN_JSON}" \
        | tr '\n' ' ' \
        | sed -n 's/.*"stop_hook_active"[[:space:]]*:[[:space:]]*true.*/FOUND/p'
)"
if [ "${STOP_HOOK_ACTIVE}" = "FOUND" ]; then
    advisory_report
    exit 0
fi

# Guard 2: an in_progress phase must exist. Merely complete<total is a normal
# state and must NOT block (issue #178 lesson).
if [ "$IN_PROGRESS" -le 0 ]; then
    advisory_report
    exit 0
fi

# ledger_line_count: total lines across all <plan-dir>/ledger-*.jsonl files.
# Echoes a single integer (0 when no ledger files exist).
ledger_line_count() {
    _total=0
    for _lf in "${PLAN_DIR}"/ledger-*.jsonl; do
        [ -f "${_lf}" ] || continue
        _n="$(grep -c '' "${_lf}" 2>/dev/null || echo 0)"
        _total=$((_total + _n))
    done
    printf "%s" "${_total}"
}

CAP="${PWF_GATE_CAP:-20}"
case "${CAP}" in
    ''|*[!0-9]*) CAP=20 ;;
esac

BLOCKS_FILE="${PLAN_DIR}/.stop_blocks"
BLOCKS="$(cat "${BLOCKS_FILE}" 2>/dev/null || echo 0)"
case "${BLOCKS}" in
    ''|*[!0-9]*) BLOCKS=0 ;;
esac

LEDGER_FILE="${PLAN_DIR}/.gate_last_ledger"
LEDGER_PREV="$(cat "${LEDGER_FILE}" 2>/dev/null || echo 0)"
case "${LEDGER_PREV}" in
    ''|*[!0-9]*) LEDGER_PREV=0 ;;
esac
LEDGER_NOW="$(ledger_line_count)"

# Guard 4: block-count cap. At or over the cap, allow the stop.
if [ "${BLOCKS}" -ge "${CAP}" ]; then
    advisory_report
    echo "[planning-with-files] gate cap reached ($BLOCKS/$CAP) — allowing stop."
    exit 0
fi

# Guard 5: stall detection. If we have blocked before (BLOCKS > 0) and the
# ledger line count has not advanced since the last block, nothing progressed:
# allow the stop instead of looping.
if [ "${BLOCKS}" -gt 0 ] && [ "${LEDGER_NOW}" -eq "${LEDGER_PREV}" ]; then
    advisory_report
    echo "[planning-with-files] no progress since last gate block — allowing stop."
    exit 0
fi

# All guards passed: block the stop.
# json_escape: escape a string for safe inclusion in a JSON string literal.
# Escapes backslash and double-quote, then neutralizes every bare control
# character JSON forbids (0x01-0x1F) by mapping it to a space. A phase heading
# may carry a literal tab or other control byte; left raw it produces invalid
# JSON ("Bad control character in string literal") that the Stop hook rejects.
json_escape() {
    printf "%s" "$1" \
        | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \
        | tr '\001-\037' ' '
}

# first_in_progress_phase: heading text of the first phase whose Status is
# in_progress. Reads the plan top-to-bottom, remembers the most recent
# "### " heading, and prints it (with the "### " prefix stripped) at the first
# in_progress status line. Plain text only — no plan body beyond the heading.
first_in_progress_phase() {
    awk '
        /^### / { heading = substr($0, 5); next }
        /\*\*Status:\*\* in_progress/ { print heading; exit }
        /\[in_progress\]/            { print heading; exit }
    ' "$PLAN_FILE"
}

PHASE_NAME="$(first_in_progress_phase)"
if [ -z "${PHASE_NAME}" ]; then
    PHASE_NAME="unknown phase"
fi
PHASE_ESCAPED="$(json_escape "${PHASE_NAME}")"

NEW_BLOCKS=$((BLOCKS + 1))
printf "%s\n" "${NEW_BLOCKS}" > "${BLOCKS_FILE}" 2>/dev/null || true
printf "%s\n" "${LEDGER_NOW}" > "${LEDGER_FILE}" 2>/dev/null || true

printf '{"decision":"block","reason":"[planning-with-files] Gated plan incomplete: phase '\''%s'\'' is in_progress (%s/%s complete, gate block %s/%s). Finish or update the plan, then stop."}\n' \
    "${PHASE_ESCAPED}" "${COMPLETE}" "${TOTAL}" "${NEW_BLOCKS}" "${CAP}"
exit 0
