"""Classify interrupt tool_result outputs into three categories.

Key finding: Corrective guidance is NOT always in a separate user message.
Claude Code embeds the user's correction directly in the tool_result.output.

Three interrupt output patterns:
  1. INLINE GUIDANCE (176): "...was rejected... To tell you how to proceed, the user said:\n<correction>"
  2. REJECTED ONLY (302): "...was rejected (eg. if it was a file edit, the new_string was NOT written to the file)"
     - 76% followed by a separate user message with guidance
     - 17% end of session
     - 7% AI continues without user input
  3. STOP ONLY (37): "The user doesn't want to take this action right now. STOP what you are doing..."

This means: to extract post-interrupt corrective action, you must check BOTH:
  a) The tool_result.output itself (after "the user said:")
  b) The next user message (if tool_result didn't contain inline guidance)

Run: /home/sagar/trace/.venv/bin/python3 analysis-14022026/session_view_scripts/research/06_interrupt_output_taxonomy.py
"""
import json
import os

DIR = os.path.join(os.path.dirname(__file__), "../viewer/public/data/sessions")
IDX = os.path.join(os.path.dirname(__file__), "../viewer/public/data/index.json")

CLAUDE_REJECTED_SIG = "doesn't want to proceed"
STOP_SIG = "doesn't want to take this action"
INLINE_GUIDANCE_SIG = "the user said:"

with open(IDX) as f:
    index = json.load(f)


# ---- Part 1: Categorize all interrupt outputs ----
print("=" * 60)
print("PART 1: Interrupt output taxonomy")
print("=" * 60)

inline_guidance = []
rejected_only = []
stop_only = []
other = []

for dev in index["developers"].values():
    for s in dev["sessions"]:
        if s.get("interruptions", 0) == 0:
            continue
        fpath = os.path.join(DIR, f'{s["id"]}.json')
        if not os.path.exists(fpath):
            continue
        with open(fpath) as f2:
            msgs = json.load(f2)

        for i, m in enumerate(msgs):
            if m["type"] != "tool_result":
                continue
            output = m.get("result", {}).get("output", "")
            if CLAUDE_REJECTED_SIG not in output and STOP_SIG not in output:
                continue

            # Find the interrupted tool (walk backwards)
            tool_name = "unknown"
            for j in range(i - 1, -1, -1):
                if msgs[j]["type"] == "tool_call" and msgs[j].get("tool"):
                    tool_name = msgs[j]["tool"]["tool_name"]
                    break

            pos_pct = round(i / max(len(msgs), 1) * 100, 1)

            entry = {
                "session_id": s["id"],
                "source": s["source"],
                "msg_index": i,
                "total_msgs": len(msgs),
                "pos_pct": pos_pct,
                "tool": tool_name,
                "output": output[:500],
            }

            if INLINE_GUIDANCE_SIG.lower() in output.lower():
                idx = output.lower().index(INLINE_GUIDANCE_SIG.lower())
                guidance = output[idx + len(INLINE_GUIDANCE_SIG) :].strip()
                entry["guidance"] = guidance
                inline_guidance.append(entry)
            elif STOP_SIG in output:
                stop_only.append(entry)
            elif "was rejected" in output:
                rejected_only.append(entry)
            else:
                other.append(entry)

print(f"INLINE guidance (correction in tool_result.output): {len(inline_guidance)}")
print(f"REJECTED only (no inline correction): {len(rejected_only)}")
print(f"STOP only (hard stop): {len(stop_only)}")
print(f"Other: {len(other)}")

# ---- Part 2: Show inline guidance examples ----
print("\n" + "=" * 60)
print("PART 2: Inline guidance examples (correction embedded in output)")
print("=" * 60)

for x in inline_guidance[:10]:
    print(f'\n  [{x["session_id"][:8]}] interrupted {x["tool"]} at {x["pos_pct"]}%')
    print(f"    Guidance: {repr(x['guidance'][:200])}")

# ---- Part 3: What follows a "rejected only" interrupt? ----
print("\n" + "=" * 60)
print('PART 3: What follows "rejected only" interrupts?')
print("=" * 60)

followup_counts = {
    "next_user_msg": 0,
    "next_assistant": 0,
    "end_of_session": 0,
}
followup_user_examples = []

for dev in index["developers"].values():
    for s in dev["sessions"]:
        if s.get("interruptions", 0) == 0:
            continue
        fpath = os.path.join(DIR, f'{s["id"]}.json')
        if not os.path.exists(fpath):
            continue
        with open(fpath) as f2:
            msgs = json.load(f2)

        for i, m in enumerate(msgs):
            if m["type"] != "tool_result":
                continue
            output = m.get("result", {}).get("output", "")
            if CLAUDE_REJECTED_SIG not in output:
                continue
            if INLINE_GUIDANCE_SIG.lower() in output.lower():
                continue
            if STOP_SIG in output:
                continue

            # Find next non-interrupt message
            for j in range(i + 1, len(msgs)):
                nxt = msgs[j]
                nxt_out = (
                    nxt.get("result", {}).get("output", "")
                    if nxt["type"] == "tool_result"
                    else ""
                )
                # Skip consecutive interrupt results
                if nxt["type"] == "tool_result" and (
                    CLAUDE_REJECTED_SIG in nxt_out or STOP_SIG in nxt_out
                ):
                    continue

                if nxt["type"] == "user":
                    followup_counts["next_user_msg"] += 1
                    if len(followup_user_examples) < 5:
                        followup_user_examples.append(
                            (s["id"][:8], nxt.get("content", "")[:200])
                        )
                elif nxt["type"] == "assistant":
                    followup_counts["next_assistant"] += 1
                else:
                    followup_counts["end_of_session"] += 1
                break
            else:
                followup_counts["end_of_session"] += 1

print(f"Followup distribution: {followup_counts}")
total_rejected = sum(followup_counts.values())
if total_rejected:
    print(
        f"  {round(followup_counts['next_user_msg'] / total_rejected * 100)}% → user gives guidance in separate message"
    )
    print(
        f"  {round(followup_counts['end_of_session'] / total_rejected * 100)}% → session ends (user abandoned)"
    )
    print(
        f"  {round(followup_counts['next_assistant'] / total_rejected * 100)}% → AI continues without user input"
    )

print("\nUser messages following rejected-only interrupts:")
for sid, content in followup_user_examples:
    print(f"  [{sid}] {repr(content)}")

# ---- Part 4: Summary ----
print("\n" + "=" * 60)
print("SUMMARY: How to extract post-interrupt corrective action")
print("=" * 60)
print(
    """
To get the user's corrective guidance after an interruption:

1. FIRST check the tool_result.output itself:
   - Look for "the user said:" in the output
   - Everything after that marker IS the user's correction
   - This covers 176 of 517 interrupt events (34%)

2. IF no inline guidance, check the next user message:
   - Skip over any consecutive interrupt tool_results
     (there can be multiple: "was rejected" + "doesn't want to take this action")
   - The next msg_type='user' message is the correction
   - This covers 232 more events (45%)

3. IF no next user message → session ended (user abandoned)
   - 51 events (10%)

4. STOP signals ("doesn't want to take this action right now. STOP")
   - 37 events (7%) — these are hard stops, usually paired with a rejected
   - Often followed by another interrupt result, then eventually a user msg

Combined: ~79% of interrupts have extractable corrective guidance.
"""
)
