#!/usr/bin/env bash
# PreCompact hook — runs just before Claude Code compacts the conversation.
#
# Claude Code does NOT let a PreCompact hook inject text into the compaction
# model (no additionalContext for this event). So instead of trying to "pass
# what to keep" into the summary, we SNAPSHOT the volatile chain state to a
# durable file. The SessionStart hook (which also fires on the `compact`
# trigger) re-injects that snapshot right after compaction — so the active task,
# decisions, anti-patterns, and uncommitted-diff survive the context squeeze.
#
# Automatic compaction is allowed only when the transcript estimate is near the
# configured context budget. Manual compaction always passes. If the transcript
# or budget cannot be read, the hook stays permissive so true limit recovery is
# never made worse.
#
# Never blocks manual/unknown compaction; never fails the session. Exit 0 always.
set -uo pipefail

INPUT="$(cat 2>/dev/null || true)"

# Resolve the project dir from the hook payload, falling back to PWD.
CWD="$(printf '%s' "$INPUT" | python3 -c 'import sys, json
try:
    print(json.load(sys.stdin).get("cwd", "") or "")
except Exception:
    print("")' 2>/dev/null || true)"
[ -z "${CWD:-}" ] && CWD="$PWD"

TRIGGER="$(printf '%s' "$INPUT" | python3 -c 'import sys, json
try:
    data = json.load(sys.stdin)
    print(data.get("trigger") or data.get("compaction_trigger") or "auto")
except Exception:
    print("auto")' 2>/dev/null || true)"

TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | python3 -c 'import sys, json
try:
    print(json.load(sys.stdin).get("transcript_path", "") or "")
except Exception:
    print("")' 2>/dev/null || true)"

# Walk up to find .hyperflow/ (stop at git root or filesystem root).
HF_DIR=""
d="$CWD"
while [ "$d" != "/" ] && [ -n "$d" ]; do
  if [ -d "$d/.hyperflow" ]; then HF_DIR="$d/.hyperflow"; break; fi
  [ -d "$d/.git" ] && break
  d="$(dirname "$d")"
done

# Nothing to preserve if the project isn't scaffolded.
[ -z "$HF_DIR" ] && exit 0

ROOT="$(dirname "$HF_DIR")"
SNAP="$HF_DIR/.precompact.md"

CONTEXT_DECISION="$(python3 - "$TRIGGER" "$TRANSCRIPT_PATH" "${HOME:-}" <<'PYEOF' 2>/dev/null || true
import json
import math
import os
import sys

trigger, transcript_path, home = sys.argv[1:4]
if trigger != "auto" or not transcript_path or not os.path.isfile(transcript_path):
    sys.exit(0)

config_path = os.path.join(home, ".hyperflow", "config.json") if home else ""
context_cfg = {}
try:
    with open(config_path) as f:
        cfg = json.load(f)
    raw = cfg.get("context", {})
    if isinstance(raw, dict):
        context_cfg = raw
except Exception:
    context_cfg = {}

def as_int(value, default, minimum, maximum=None):
    try:
        parsed = int(value)
    except Exception:
        return default
    if parsed < minimum:
        return default
    if maximum is not None and parsed > maximum:
        return default
    return parsed

window = as_int(
    os.environ.get("HYPERFLOW_CONTEXT_WINDOW_TOKENS") or context_cfg.get("windowTokens"),
    200000,
    10000,
)
min_percent = as_int(
    os.environ.get("HYPERFLOW_AUTO_COMPACT_MIN_PERCENT") or context_cfg.get("autoCompactMinPercent"),
    72,
    1,
    99,
)

def strings_from_message(value):
    if isinstance(value, str):
        if value.startswith("/") and len(value) < 300:
            return
        yield value
    elif isinstance(value, list):
        for item in value:
            yield from strings_from_message(item)
    elif isinstance(value, dict):
        for key, item in value.items():
            if key in {"content", "text", "thinking", "summary", "result", "message", "reason"}:
                yield from strings_from_message(item)

chars = 0
with open(transcript_path, errors="ignore") as f:
    for line in f:
        try:
            row = json.loads(line)
        except Exception:
            continue
        if not isinstance(row, dict):
            continue
        for key in ("message", "content", "tool_input", "tool_result", "summary"):
            if key in row:
                for text in strings_from_message(row[key]):
                    chars += len(text)

if chars <= 0:
    sys.exit(0)

# Conservative approximation for mixed prose/code/tool transcripts.
tokens = math.ceil(chars / 3.6)
percent = min(100, math.ceil(tokens * 100 / window))
if percent < min_percent:
    reason = (
        f"Hyperflow skipped automatic compaction: estimated context usage is "
        f"{percent}% ({tokens:,}/{window:,} tokens), below the configured "
        f"{min_percent}% threshold. Continue the run; compact closer to the limit."
    )
    print(json.dumps({"decision": "block", "reason": reason}))
PYEOF
)"

{
  printf '<!-- hyperflow precompact snapshot · %s · trigger=%s -->\n' "$(date -u +%FT%TZ 2>/dev/null || echo now)" "${TRIGGER:-auto}"
  printf '## Recovered context (post-compaction)\n'
  printf 'The conversation was just compacted mid-run. Re-orient from the durable state below before continuing — do not assume earlier turns are still in context.\n\n'

  # 1 — Active task + chain state
  if ls "$HF_DIR/tasks/"*.md >/dev/null 2>&1; then
    printf '### Active task files (resume here)\n'
    for f in "$HF_DIR/tasks/"*.md; do
      [ -f "$f" ] && printf -- '- `%s`\n' "$(basename "$f")"
    done
    printf '\n'
  fi

  # 2 — Decisions / spec
  for df in project-decisions.md decisions.md; do
    if [ -f "$HF_DIR/memory/$df" ]; then
      printf '### Decisions (%s)\n' "$df"
      head -n 40 "$HF_DIR/memory/$df"
      printf '\n'
      break
    fi
  done
  if ls "$HF_DIR/specs/"*.md >/dev/null 2>&1; then
    printf '### Open specs\n'
    for f in "$HF_DIR/specs/"*.md; do
      [ -f "$f" ] && printf -- '- `%s`\n' "$(basename "$f")"
    done
    printf '\n'
  fi

  # 3 — Reviewer findings / anti-patterns (hot memory)
  if [ -f "$HF_DIR/memory/anti-patterns.md" ]; then
    printf '### Anti-patterns (hot — keep enforcing)\n'
    head -n 30 "$HF_DIR/memory/anti-patterns.md"
    printf '\n'
  fi

  # 4 — Recent edits / diff summary
  if git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
    unstaged="$(git -C "$ROOT" --no-pager diff --stat 2>/dev/null | head -n 40 || true)"
    staged="$(git -C "$ROOT" --no-pager diff --stat --cached 2>/dev/null | head -n 40 || true)"
    if [ -n "$unstaged" ] || [ -n "$staged" ]; then
      printf '### Uncommitted changes so far\n```\n'
      [ -n "$staged" ] && { printf '# staged\n'; printf '%s\n' "$staged"; }
      [ -n "$unstaged" ] && { printf '# unstaged\n'; printf '%s\n' "$unstaged"; }
      printf '```\n'
    fi
  fi
} > "$SNAP" 2>/dev/null || true

if [ -n "$CONTEXT_DECISION" ]; then
  printf '%s\n' "$CONTEXT_DECISION"
fi

exit 0
