"""CLI interface for Cozempic."""

from __future__ import annotations

import argparse
import os
import platform
import subprocess
import sys
from pathlib import Path

from .diagnosis import diagnose_session
from .doctor import run_doctor
from .executor import execute_actions, run_prescription
from .guard import checkpoint_team, start_guard, start_guard_daemon
from .init import run_init
from .recap import save_recap
from .registry import PRESCRIPTIONS, STRATEGIES
from .helpers import is_ssh_session, shell_quote
from .session import find_claude_pid, find_current_session, find_sessions, load_messages, project_slug_to_path, resolve_session, save_messages
from .tokens import estimate_session_tokens, quick_token_estimate
from .types import PrescriptionResult, StrategyResult

# Ensure all strategies are registered
import cozempic.strategies  # noqa: F401

# Fix Windows stdout/stderr encoding for Unicode characters (box-drawing, emoji)
if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")


# ─── Formatting ───────────────────────────────────────────────────────────────

def fmt_bytes(b: int) -> str:
    if b < 1024:
        return f"{b}B"
    elif b < 1024 * 1024:
        return f"{b / 1024:.1f}KB"
    else:
        return f"{b / (1024 * 1024):.2f}MB"


def fmt_pct(part: int, total: int) -> str:
    if total == 0:
        return "0%"
    return f"{part / total * 100:.1f}%"


def fmt_tokens(t: int) -> str:
    if t < 1000:
        return f"{t}"
    elif t < 1_000_000:
        return f"{t / 1000:.1f}K"
    else:
        return f"{t / 1_000_000:.2f}M"


def fmt_context_bar(pct: float, width: int = 20) -> str:
    filled = int(round(pct / 100 * width))
    filled = max(0, min(filled, width))
    bar = "=" * filled + "-" * (width - filled)
    return f"[{bar}] {pct:.0f}%"


def print_diagnosis(diag: dict, path: Path):
    total = diag["total_bytes"]
    print(f"\n  Patient: {path.stem}")
    print(f"  Weight:  {fmt_bytes(total)} ({diag['total_messages']} messages)")

    te = diag.get("token_estimate")
    if te:
        confidence = f", {te.confidence}" if te.method == "heuristic" else ""
        model_str = f"  Model:   {te.model}" if te.model else ""
        window_str = f"{fmt_tokens(te.context_window)}" if te.context_window != 200_000 else "200K"
        print(f"  Tokens:  {fmt_tokens(te.total)} ({te.method}{confidence})")
        print(f"  Context: {fmt_context_bar(te.context_pct)} of {window_str}")
        if model_str:
            print(model_str)
    print()

    print("  Vital Signs:")
    print(f"    Progress ticks:     {diag['progress_count']:>6}")
    print(f"    File history snaps: {diag['file_history_count']:>6}")
    print(f"    System reminders:   {diag['reminder_count']:>6}")
    print(f"    Thinking content:   {fmt_bytes(diag['thinking_bytes']):>10} ({fmt_pct(diag['thinking_bytes'], total)})")
    print(f"    Signatures:         {fmt_bytes(diag['signature_bytes']):>10} ({fmt_pct(diag['signature_bytes'], total)})")
    print(f"    Tool results:       {fmt_bytes(diag['tool_result_bytes']):>10} ({fmt_pct(diag['tool_result_bytes'], total)})")
    print()

    print("  Message Type Breakdown:")
    sorted_types = sorted(diag["type_stats"].items(), key=lambda x: x[1]["bytes"], reverse=True)
    for mtype, stats in sorted_types:
        pct = fmt_pct(stats["bytes"], total)
        print(f"    {mtype:<28} {stats['count']:>5} msgs  {fmt_bytes(stats['bytes']):>10}  ({pct})")
    print()

    print("  Top 10 Largest Messages:")
    for size, idx, mtype, pos in diag["largest_messages"][:10]:
        print(f"    Line {idx:<6}  {mtype:<20}  {fmt_bytes(size)}")
    print()


def print_strategy_result(sr: StrategyResult, total_bytes: int):
    saved = sum(a.original_bytes - a.pruned_bytes for a in sr.actions) if sr.actions else 0
    pct = fmt_pct(saved, total_bytes)

    detail_parts = []
    if sr.messages_removed:
        detail_parts.append(f"{sr.messages_removed} removed")
    if sr.messages_replaced:
        detail_parts.append(f"{sr.messages_replaced} modified")
    detail = f" ({', '.join(detail_parts)})" if detail_parts else ""

    print(f"    {sr.strategy_name:<30} {fmt_bytes(saved):>10} saved  ({pct}){detail}  {sr.summary}")


def print_prescription_result(pr: PrescriptionResult):
    saved = pr.original_total_bytes - pr.final_total_bytes
    removed = pr.original_message_count - pr.final_message_count
    total_replaced = sum(sr.messages_replaced for sr in pr.strategy_results)

    print(f"\n  Prescription: {pr.prescription_name}")

    if pr.original_tokens is not None and pr.final_tokens is not None:
        tok_saved = pr.original_tokens - pr.final_tokens
        tok_pct = f"{tok_saved / pr.original_tokens * 100:.1f}%" if pr.original_tokens > 0 else "0%"
        from .tokens import DEFAULT_CONTEXT_WINDOW
        context_window = pr.context_window or DEFAULT_CONTEXT_WINDOW
        after_pct = round(pr.final_tokens / context_window * 100, 1)
        window_str = fmt_tokens(context_window)
        print(f"  Before: {fmt_tokens(pr.original_tokens)} tokens ({fmt_bytes(pr.original_total_bytes)}, {pr.original_message_count} messages)")
        print(f"  After:  {fmt_tokens(pr.final_tokens)} tokens ({fmt_bytes(pr.final_total_bytes)}, {pr.final_message_count} messages)")
        print(f"  Freed:  {fmt_tokens(tok_saved)} tokens ({tok_pct}) — {fmt_bytes(saved)}, {removed} removed, {total_replaced} modified")
        print(f"  Context: {fmt_context_bar(after_pct)} of {window_str}{f' ({pr.model})' if pr.model else ''}")
    else:
        byte_pct = fmt_pct(saved, pr.original_total_bytes)
        print(f"  Before: {fmt_bytes(pr.original_total_bytes)} ({pr.original_message_count} messages)")
        print(f"  After:  {fmt_bytes(pr.final_total_bytes)} ({pr.final_message_count} messages)")
        print(f"  Saved:  {fmt_bytes(saved)} ({byte_pct}) — {removed} removed, {total_replaced} modified")

    print()
    print("  Strategy Results:")
    for sr in pr.strategy_results:
        print_strategy_result(sr, pr.original_total_bytes)
    print()


# ─── Commands ─────────────────────────────────────────────────────────────────

def cmd_list(args):
    sessions = find_sessions(args.project)
    if not sessions:
        print("No sessions found.")
        return

    print(f"\n  {'Session ID':<40} {'Size':>10} {'Tokens':>8} {'Messages':>8} {'Modified':<20} Project")
    print(f"  {'─' * 40} {'─' * 10} {'─' * 8} {'─' * 8} {'─' * 20} {'─' * 30}")

    for sess in sorted(sessions, key=lambda s: s["size"], reverse=True):
        sid = sess["session_id"]
        if len(sid) > 36:
            sid = sid[:33] + "..."
        tok = quick_token_estimate(sess["path"])
        tok_str = fmt_tokens(tok) if tok is not None else "—"
        print(
            f"  {sid:<40} {fmt_bytes(sess['size']):>10} {tok_str:>8} {sess['lines']:>8}"
            f" {sess['mtime'].strftime('%Y-%m-%d %H:%M'):<20} {sess['project'][-40:]}"
        )
    print()

    total = sum(s["size"] for s in sessions)
    print(f"  Total: {len(sessions)} sessions, {fmt_bytes(total)}")
    print()


def cmd_current(args):
    cwd = args.cwd or None
    match_text = getattr(args, "match", None)
    sess = find_current_session(cwd, match_text=match_text)
    if not sess:
        print("Could not detect current session.", file=sys.stderr)
        print("Make sure you're running from a directory with a Claude Code project.", file=sys.stderr)
        sys.exit(1)

    print(f"\n  Current Session:")
    print(f"    ID:      {sess['session_id']}")
    print(f"    Size:    {fmt_bytes(sess['size'])} ({sess['lines']} messages)")

    from .tokens import detect_context_window, detect_model
    messages_for_model = load_messages(sess["path"])
    context_window = detect_context_window(messages_for_model)
    model = detect_model(messages_for_model)

    tok = quick_token_estimate(sess["path"])
    if tok is not None:
        pct = round(tok / context_window * 100, 1)
        window_str = fmt_tokens(context_window)
        print(f"    Tokens:  {fmt_tokens(tok)} {fmt_context_bar(pct)} of {window_str}")
    if model:
        print(f"    Model:   {model}")

    print(f"    Project: {sess['project']}")
    print(f"    Path:    {sess['path']}")
    print(f"    Modified: {sess['mtime'].strftime('%Y-%m-%d %H:%M:%S')}")
    print()

    if args.diagnose:
        messages = load_messages(sess["path"])
        diag = diagnose_session(messages)
        print_diagnosis(diag, sess["path"])

        print("  Estimated Savings by Prescription:")
        for rx_name, strategy_names in PRESCRIPTIONS.items():
            new_msgs, _ = run_prescription(messages, strategy_names, {})
            final_bytes = sum(b for _, _, b in new_msgs)
            total_saved = diag["total_bytes"] - final_bytes
            pct = fmt_pct(total_saved, diag["total_bytes"])
            print(f"    {rx_name:<15} ~{fmt_bytes(total_saved):>10} ({pct})")
        print()


def cmd_diagnose(args):
    path = resolve_session(args.session, getattr(args, "project", None))
    messages = load_messages(path)
    diag = diagnose_session(messages)
    print_diagnosis(diag, path)

    print("  Estimated Savings by Prescription:")
    for rx_name, strategy_names in PRESCRIPTIONS.items():
        new_msgs, _ = run_prescription(messages, strategy_names, {})
        final_bytes = sum(b for _, _, b in new_msgs)
        total_saved = diag["total_bytes"] - final_bytes
        pct = fmt_pct(total_saved, diag["total_bytes"])
        print(f"    {rx_name:<15} ~{fmt_bytes(total_saved):>10} ({pct})")
    print()


def cmd_treat(args):
    path = resolve_session(args.session, getattr(args, "project", None), strict=getattr(args, "execute", False))
    messages = load_messages(path)
    rx_name = args.rx or "standard"

    if rx_name not in PRESCRIPTIONS:
        print(f"Error: Unknown prescription '{rx_name}'. Options: {', '.join(PRESCRIPTIONS)}", file=sys.stderr)
        sys.exit(1)

    strategy_names = PRESCRIPTIONS[rx_name]
    config = {}
    if args.thinking_mode:
        config["thinking_mode"] = args.thinking_mode

    original_bytes = sum(b for _, _, b in messages)
    original_count = len(messages)

    # Token estimate before pruning
    pre_te = estimate_session_tokens(messages)

    new_messages, strategy_results = run_prescription(messages, strategy_names, config)
    final_bytes = sum(b for _, _, b in new_messages)
    final_count = len(new_messages)

    # Token estimate after pruning
    post_te = estimate_session_tokens(new_messages)

    pr = PrescriptionResult(
        prescription_name=rx_name,
        strategy_results=strategy_results,
        original_total_bytes=original_bytes,
        final_total_bytes=final_bytes,
        original_message_count=original_count,
        final_message_count=final_count,
        original_tokens=pre_te.total,
        final_tokens=post_te.total,
        token_method=pre_te.method,
        model=pre_te.model,
        context_window=pre_te.context_window,
    )

    print_prescription_result(pr)

    if pre_te.method == "exact" and post_te.method == "heuristic":
        print("  Note: exact usage data was stripped — post-treatment token count is estimated.")

    if args.execute:
        backup = save_messages(path, new_messages, create_backup=True)
        print(f"  Treatment applied to {path}")
        if backup:
            print(f"  Backup: {backup}")
        print(f"  Final size: {fmt_bytes(final_bytes)}")
    else:
        print("  DRY RUN — no changes made. Use --execute to apply.")
    print()


def cmd_strategy(args):
    path = resolve_session(args.session, getattr(args, "project", None), strict=getattr(args, "execute", False))
    messages = load_messages(path)

    if args.name not in STRATEGIES:
        print(f"Error: Unknown strategy '{args.name}'.", file=sys.stderr)
        print(f"Available: {', '.join(sorted(STRATEGIES))}", file=sys.stderr)
        sys.exit(1)

    config = {}
    if args.thinking_mode:
        config["thinking_mode"] = args.thinking_mode

    original_bytes = sum(b for _, _, b in messages)
    sr = STRATEGIES[args.name].func(messages, config)

    saved = sum(a.original_bytes - a.pruned_bytes for a in sr.actions)
    print(f"\n  Strategy: {sr.strategy_name}")
    print(f"  Savings: {fmt_bytes(saved)} ({fmt_pct(saved, original_bytes)})")
    print(f"  Actions: {len(sr.actions)} ({sr.messages_removed} removed, {sr.messages_replaced} modified)")
    print(f"  Summary: {sr.summary}")
    print()

    if args.verbose:
        for a in sr.actions[:20]:
            print(f"    Line {a.line_index:<6} {a.action:<8} {fmt_bytes(a.original_bytes):>10} -> {fmt_bytes(a.pruned_bytes):>10}  {a.reason}")
        if len(sr.actions) > 20:
            print(f"    ... and {len(sr.actions) - 20} more actions")
        print()

    if args.execute:
        new_messages = execute_actions(messages, sr.actions)
        backup = save_messages(path, new_messages, create_backup=True)
        final_bytes = sum(b for _, _, b in new_messages)
        print(f"  Applied. Final size: {fmt_bytes(final_bytes)}")
        if backup:
            print(f"  Backup: {backup}")
    else:
        print("  DRY RUN — no changes made. Use --execute to apply.")
    print()


def cmd_reload(args):
    """Treat the current session, then spawn a watcher that auto-resumes Claude."""
    cwd = args.cwd or os.getcwd()
    sess = find_current_session(cwd, strict=True)
    if not sess:
        print("Could not detect current session.", file=sys.stderr)
        print("Cannot determine session unambiguously — use an explicit session ID.", file=sys.stderr)
        print("Make sure you're running from a directory with a Claude Code project.", file=sys.stderr)
        sys.exit(1)

    # Derive project directory from session slug (more reliable than CWD)
    project_dir = project_slug_to_path(sess["project"])
    if os.path.isdir(project_dir):
        cwd = project_dir

    rx_name = args.rx or "standard"
    if rx_name not in PRESCRIPTIONS:
        print(f"Error: Unknown prescription '{rx_name}'. Options: {', '.join(PRESCRIPTIONS)}", file=sys.stderr)
        sys.exit(1)

    # Step 1: Apply treatment
    path = sess["path"]
    messages = load_messages(path)
    strategy_names = PRESCRIPTIONS[rx_name]
    config = {}
    if args.thinking_mode:
        config["thinking_mode"] = args.thinking_mode

    original_bytes = sum(b for _, _, b in messages)
    original_count = len(messages)

    # Token estimate before pruning
    pre_te = estimate_session_tokens(messages)

    new_messages, strategy_results = run_prescription(messages, strategy_names, config)
    final_bytes = sum(b for _, _, b in new_messages)
    final_count = len(new_messages)

    # Token estimate after pruning
    post_te = estimate_session_tokens(new_messages)

    pr = PrescriptionResult(
        prescription_name=rx_name,
        strategy_results=strategy_results,
        original_total_bytes=original_bytes,
        final_total_bytes=final_bytes,
        original_message_count=original_count,
        final_message_count=final_count,
        original_tokens=pre_te.total,
        final_tokens=post_te.total,
        token_method=pre_te.method,
        model=pre_te.model,
        context_window=pre_te.context_window,
    )
    print_prescription_result(pr)

    backup = save_messages(path, new_messages, create_backup=True)
    print(f"  Treatment applied to {path}")
    if backup:
        print(f"  Backup: {backup}")
    print(f"  Final size: {fmt_bytes(final_bytes)}")
    print()

    # Step 2: Generate recap from the pruned messages
    import tempfile
    recap_path = Path(tempfile.gettempdir()) / f"cozempic_recap_{sess['session_id'][:8]}.txt"
    save_recap(new_messages, recap_path)
    print(f"  Recap saved to {recap_path}")

    # Step 3: Find Claude's parent PID and spawn watcher
    claude_pid = find_claude_pid()
    if not claude_pid:
        print("  WARNING: Could not detect Claude Code process.")
        print("  Treatment was applied, but auto-resume watcher was NOT started.")
        print("  Restart Claude manually with: claude --resume")
        return

    _spawn_watcher(claude_pid, cwd, recap_path=recap_path, session_id=sess["session_id"])
    print(f"  Watcher spawned (watching Claude PID {claude_pid}).")
    print(f"  Now type /exit — a new terminal will open with 'claude --resume'.")
    print()


def _spawn_watcher(claude_pid: int, project_dir: str, recap_path: Path | None = None, session_id: str | None = None):
    """Spawn a detached background process that waits for Claude to exit, then resumes."""
    resume_flag = f"--resume {session_id}" if session_id else "--resume"

    # SSH sessions can't open GUI terminals — tell the user to resume manually
    if is_ssh_session():
        print(f"  SSH session detected — auto-resume is not available over SSH.")
        print(f"  After exiting, resume manually:")
        print(f"    cd {project_dir} && claude {resume_flag}")
        return

    system = platform.system()

    # Build the command sequence: show recap, then launch claude --resume
    recap_cmd = ""
    if recap_path and recap_path.exists():
        recap_cmd = f"cat {shell_quote(str(recap_path))}; echo; "

    if system == "Darwin":
        inner_cmd = f"cd {shell_quote(project_dir)} && {recap_cmd}claude {resume_flag}"
        resume_cmd = (
            f"osascript -e 'tell application \"Terminal\" to do script "
            f"\"{inner_cmd}\"'"
        )
    elif system == "Linux":
        inner_cmd = f"cd {shell_quote(project_dir)} && {recap_cmd}claude {resume_flag}; exec bash"
        resume_cmd = (
            f"if command -v gnome-terminal >/dev/null 2>&1; then "
            f"gnome-terminal -- bash -c '{inner_cmd}'; "
            f"elif command -v xterm >/dev/null 2>&1; then "
            f"xterm -e '{inner_cmd}' & "
            f"else echo 'No terminal emulator found' >> /tmp/cozempic_reload.log; fi"
        )
    else:
        print(f"  WARNING: Auto-resume not supported on {system}.")
        print(f"  Restart manually: cd {project_dir} && claude {resume_flag}")
        return

    watcher_script = (
        f"while kill -0 {claude_pid} 2>/dev/null; do sleep 1; done; "
        f"sleep 1; "
        f"{resume_cmd}; "
        f"echo \"$(date): Cozempic resumed Claude in {project_dir}\" >> /tmp/cozempic_reload.log"
    )

    subprocess.Popen(
        ["bash", "-c", watcher_script],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        stdin=subprocess.DEVNULL,
        start_new_session=True,  # fully detach from parent
    )


def cmd_checkpoint(args):
    """Save team/agent state from the current session. No pruning."""
    state = checkpoint_team(cwd=args.cwd or os.getcwd())
    if state and not state.is_empty():
        if args.show:
            print()
            print(state.to_recovery_text())
            print()


def cmd_post_compact(args):
    """Output recovery context after native compaction. Designed for PostCompact hook.

    Reads team-checkpoint.md saved by PreCompact and outputs it to stdout,
    where Claude Code picks it up as hook feedback. Silent when no checkpoint
    exists (solo sessions without agent teams).
    """
    from .team import read_team_checkpoint

    cwd = args.cwd or os.getcwd()

    # Try to find project dir from current session
    sess = find_current_session(cwd)
    project_dir = Path(sess["path"]).parent if sess else Path(cwd)

    content = read_team_checkpoint(project_dir)
    if content:
        print(content)


def cmd_guard(args):
    """Start the guard daemon to prevent compaction-induced state loss."""
    session_id = args.session or None

    if getattr(args, "system_overhead_tokens", None):
        os.environ["COZEMPIC_SYSTEM_OVERHEAD_TOKENS"] = str(args.system_overhead_tokens)

    if args.daemon:
        result = start_guard_daemon(
            cwd=args.cwd or os.getcwd(),
            threshold_mb=args.threshold,
            soft_threshold_mb=args.soft_threshold,
            rx_name=args.rx or "standard",
            interval=args.interval,
            auto_reload=not args.no_reload,
            reactive=not args.no_reactive,
            threshold_tokens=args.threshold_tokens,
            soft_threshold_tokens=args.soft_threshold_tokens,
            session_id=session_id,
        )
        if result["already_running"]:
            print(f"  Guard already running (PID {result['pid']})")
        elif result["started"]:
            print(f"  Guard daemon started (PID {result['pid']})")
            print(f"  Log: {result['log_file']}")
        return

    start_guard(
        cwd=args.cwd or os.getcwd(),
        threshold_mb=args.threshold,
        soft_threshold_mb=args.soft_threshold,
        rx_name=args.rx or "standard",
        interval=args.interval,
        auto_reload=not args.no_reload,
        reactive=not args.no_reactive,
        threshold_tokens=args.threshold_tokens,
        soft_threshold_tokens=args.soft_threshold_tokens,
        session_id=session_id,
    )


def cmd_doctor(args):
    """Run health checks on Claude Code configuration and sessions."""
    STATUS_ICONS = {
        "ok": "✓",
        "warning": "⚠",
        "issue": "✗",
        "fixed": "→",
    }
    STATUS_COLORS = {
        "ok": "",
        "warning": "",
        "issue": "",
        "fixed": "",
    }

    results = run_doctor(fix=args.fix)

    print("\n  COZEMPIC DOCTOR")
    print("  ═══════════════════════════════════════════════════════════════════")
    print()

    issues = 0
    warnings = 0
    fixed = 0

    for r in results:
        icon = STATUS_ICONS.get(r.status, "?")
        print(f"    {icon} {r.name:<25} [{r.status.upper()}]")
        print(f"      {r.message}")
        if r.fix_description and r.status not in ("ok", "fixed"):
            print(f"      Fix: {r.fix_description}")
        print()

        if r.status == "issue":
            issues += 1
        elif r.status == "warning":
            warnings += 1
        elif r.status == "fixed":
            fixed += 1

    # Summary
    if fixed:
        print(f"  Summary: {fixed} issue(s) fixed")
    elif issues or warnings:
        print(f"  Summary: {issues} issue(s), {warnings} warning(s)")
        if not args.fix:
            print("  Run 'cozempic doctor --fix' to auto-fix where possible.")
    else:
        print("  All clear — no issues found.")
    print()


def cmd_init(args):
    """Wire cozempic hooks and slash command into the current project."""
    project_dir = args.cwd or os.getcwd()

    print(f"\n  COZEMPIC INIT")
    print(f"  ═══════════════════════════════════════════════════════════════════")
    print(f"  Project: {project_dir}")
    print()

    result = run_init(project_dir, skip_slash=args.no_slash_command)

    # Report hooks
    hooks = result["hooks"]
    if hooks["added"]:
        print(f"  Hooks added to {hooks['settings_path']}:")
        for h in hooks["added"]:
            print(f"    + {h}")
        if hooks["backup_path"]:
            print(f"  Backup: {hooks['backup_path']}")
    else:
        print(f"  Hooks: already configured (nothing to add)")

    if hooks["skipped"]:
        for h in hooks["skipped"]:
            print(f"    ~ {h} (already exists, skipped)")

    print()

    # Report slash command
    slash = result["slash_command"]
    if slash.get("updated"):
        print(f"  Slash command: updated → {slash['path']}")
        print(f"  Use /cozempic in any Claude Code session to diagnose and treat.")
    elif slash["installed"]:
        print(f"  Slash command: installed → {slash['path']}")
        print(f"  Use /cozempic in any Claude Code session to diagnose and treat.")
    elif slash["already_existed"]:
        print(f"  Slash command: up-to-date at {slash['path']}")
    elif not args.no_slash_command:
        print(f"  Slash command: source not found (install from git repo to get it)")

    print()

    # Summary: what to do next
    print(f"  Setup complete. Protection is fully automatic:")
    print(f"    - Guard daemon auto-starts on every session (SessionStart hook)")
    print(f"    - Team state checkpointed on every agent event (PostToolUse hooks)")
    print(f"    - Emergency checkpoint before compaction (PreCompact hook)")
    print(f"    - Recovery context after compaction (PostCompact hook)")
    print(f"    - Final checkpoint on session end (Stop hook)")
    print()
    print(f"  Just start Claude Code normally. No second terminal needed.")
    print()


def cmd_completions(args):
    """Generate shell completion scripts."""
    from .completion import bash_completion, zsh_completion
    if args.shell == "bash":
        print(bash_completion())
    elif args.shell == "zsh":
        print(zsh_completion())


def cmd_formulary(args):
    print("\n  COZEMPIC FORMULARY")
    print("  ═══════════════════════════════════════════════════════════════════")
    print()
    print("  Strategies:")
    print(f"  {'#':<4} {'Name':<30} {'Tier':<12} {'Expected':>10}  Description")
    print(f"  {'─' * 4} {'─' * 30} {'─' * 12} {'─' * 10}  {'─' * 40}")
    for i, (name, info) in enumerate(STRATEGIES.items(), 1):
        print(f"  {i:<4} {name:<30} {info.tier:<12} {info.expected_savings:>10}  {info.description}")
    print()

    print("  Prescriptions:")
    for rx_name, strategy_names in PRESCRIPTIONS.items():
        names = ", ".join(strategy_names)
        print(f"    {rx_name:<15} [{len(strategy_names)} strategies] {names}")
    print()

    print("  Usage:")
    print("    cozempic treat <session> -rx gentle      # Safe, minimal pruning")
    print("    cozempic treat <session> -rx standard     # Recommended (default)")
    print("    cozempic treat <session> -rx aggressive   # Maximum savings")
    print("    cozempic treat <session> --execute        # Apply (default is dry-run)")
    print()


# ─── Parser ───────────────────────────────────────────────────────────────────

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="cozempic",
        description="Context weight-loss tool for Claude Code — prune bloated JSONL conversation files",
    )
    parser.add_argument("--version", action="version", version="%(prog)s 1.2.2")
    parser.add_argument("--context-window", type=int, default=None, help="Override context window size in tokens (e.g. 1000000 for 1M beta)")
    parser.add_argument("--system-overhead-tokens", type=int, default=None, help="Override system overhead estimate (default: 21000). Increase for heavy rules/MCP configs.")
    sub = parser.add_subparsers(dest="command")

    session_help = "Session ID, UUID prefix, path, or 'current' for auto-detect"

    # list
    p_list = sub.add_parser("list", help="List sessions with sizes")
    p_list.add_argument("--project", help="Filter by project name")

    # current
    p_current = sub.add_parser("current", help="Show current session for this project")
    p_current.add_argument("--cwd", help="Working directory (default: current)")
    p_current.add_argument("--match", help="Text snippet to match against session content (for multi-session disambiguation)")
    p_current.add_argument("--diagnose", "-d", action="store_true", help="Also run diagnosis")

    # diagnose
    p_diag = sub.add_parser("diagnose", help="Analyze bloat sources (read-only)")
    p_diag.add_argument("session", help=session_help)
    p_diag.add_argument("--project", help="Filter by project name")

    # treat
    p_treat = sub.add_parser("treat", help="Run prescription (dry-run by default)")
    p_treat.add_argument("session", help=session_help)
    p_treat.add_argument("-rx", help="Prescription: gentle, standard, aggressive")
    p_treat.add_argument("--execute", action="store_true", help="Apply changes (default is dry-run)")
    p_treat.add_argument("--project", help="Filter by project name")
    p_treat.add_argument("--thinking-mode", choices=["remove", "truncate", "signature-only"], help="Thinking block mode")

    # strategy
    p_strat = sub.add_parser("strategy", help="Run single strategy")
    p_strat.add_argument("name", help="Strategy name")
    p_strat.add_argument("session", help=session_help)
    p_strat.add_argument("--execute", action="store_true", help="Apply changes")
    p_strat.add_argument("--verbose", "-v", action="store_true", help="Show action details")
    p_strat.add_argument("--project", help="Filter by project name")
    p_strat.add_argument("--thinking-mode", choices=["remove", "truncate", "signature-only"])

    # reload
    p_reload = sub.add_parser("reload", help="Treat current session and auto-resume after exit")
    p_reload.add_argument("--cwd", help="Working directory (default: current)")
    p_reload.add_argument("-rx", help="Prescription: gentle, standard, aggressive (default: standard)")
    p_reload.add_argument("--thinking-mode", choices=["remove", "truncate", "signature-only"])

    # checkpoint
    p_cp = sub.add_parser("checkpoint", help="Save team/agent state from the current session (no pruning)")
    p_cp.add_argument("--cwd", help="Working directory (default: current)")
    p_cp.add_argument("--show", action="store_true", help="Print the team state after saving")

    # post-compact
    p_post_compact = sub.add_parser("post-compact", help="Output team state after compaction (for PostCompact hook)")
    p_post_compact.add_argument("--cwd", help="Working directory (default: current)")

    # guard
    p_guard = sub.add_parser("guard", help="Background sentinel — auto-prune before compaction triggers")
    p_guard.add_argument("--cwd", help="Working directory (default: current)")
    p_guard.add_argument("-rx", help="Prescription to apply (default: standard)")
    p_guard.add_argument("--threshold", type=float, default=50.0, help="Hard threshold in MB — full prune + reload (default: 50)")
    p_guard.add_argument("--soft-threshold", type=float, default=None, help="Soft threshold in MB — gentle prune, no reload (default: 60%% of --threshold)")
    p_guard.add_argument("--interval", type=int, default=30, help="Check interval in seconds (default: 30)")
    p_guard.add_argument("--threshold-tokens", type=int, default=None, help="Hard threshold in tokens (default: 75%% of context window)")
    p_guard.add_argument("--soft-threshold-tokens", type=int, default=None, help="Soft threshold in tokens (default: 45%% of context window)")
    p_guard.add_argument("--no-reload", action="store_true", help="Prune without auto-reload at hard threshold")
    p_guard.add_argument("--no-reactive", action="store_true", help="Disable reactive overflow recovery (kqueue/polling watcher)")
    p_guard.add_argument("--daemon", action="store_true", help="Run in background (PID file prevents double-starts)")
    p_guard.add_argument("--session", help="Explicit session ID or path (bypasses auto-detection)")
    p_guard.add_argument("--system-overhead-tokens", type=int, default=None, help="Override system overhead token estimate (default: 21000). Increase for heavy configs with many rules files, MCP servers, or large CLAUDE.md")

    # init
    p_init = sub.add_parser("init", help="Auto-wire hooks and slash command into this project")
    p_init.add_argument("--cwd", help="Project directory (default: current)")
    p_init.add_argument("--no-slash-command", action="store_true", help="Skip installing /cozempic slash command")

    # doctor
    p_doctor = sub.add_parser("doctor", help="Check for known Claude Code issues and fix them")
    p_doctor.add_argument("--fix", action="store_true", help="Auto-fix issues where possible")

    # formulary
    sub.add_parser("formulary", help="Show all strategies & prescriptions")

    # completions
    p_comp = sub.add_parser("completions", help="Generate shell completion script")
    p_comp.add_argument("shell", choices=["bash", "zsh"], help="Shell type")

    return parser


_SUBCOMMANDS = {
    "list", "current", "diagnose", "treat", "strategy", "reload",
    "checkpoint", "post-compact", "guard", "init", "doctor", "formulary", "completions",
}


def _prescan_argv(argv: list[str]) -> list[str]:
    """Extract global flags from anywhere in argv, setting env vars as a side effect.

    Argparse requires root-level flags before the subcommand name; this lets users
    put --context-window and --system-overhead-tokens anywhere (#13).
    Returns cleaned argv with those flags removed so argparse sees the rest normally.
    """
    cleaned: list[str] = []
    i = 0
    sub_seen = False
    while i < len(argv):
        tok = argv[i]
        if not sub_seen and tok in _SUBCOMMANDS:
            sub_seen = True
            cleaned.append(tok)
            i += 1
            continue
        if sub_seen:
            if tok == "--context-window" and i + 1 < len(argv):
                val = argv[i + 1]
                try:
                    if int(val) <= 0:
                        raise ValueError
                    os.environ["COZEMPIC_CONTEXT_WINDOW"] = val
                except ValueError:
                    print(f"Warning: ignoring invalid --context-window '{val}'", file=sys.stderr)
                i += 2
                continue
            if tok.startswith("--context-window="):
                val = tok.split("=", 1)[1]
                try:
                    if int(val) <= 0:
                        raise ValueError
                    os.environ["COZEMPIC_CONTEXT_WINDOW"] = val
                except ValueError:
                    print(f"Warning: ignoring invalid --context-window '{val}'", file=sys.stderr)
                i += 1
                continue
            if tok == "--system-overhead-tokens" and i + 1 < len(argv):
                val = argv[i + 1]
                try:
                    if int(val) <= 0:
                        raise ValueError
                    os.environ["COZEMPIC_SYSTEM_OVERHEAD_TOKENS"] = val
                except ValueError:
                    print(f"Warning: ignoring invalid --system-overhead-tokens '{val}'", file=sys.stderr)
                i += 2
                continue
            if tok.startswith("--system-overhead-tokens="):
                val = tok.split("=", 1)[1]
                try:
                    if int(val) <= 0:
                        raise ValueError
                    os.environ["COZEMPIC_SYSTEM_OVERHEAD_TOKENS"] = val
                except ValueError:
                    print(f"Warning: ignoring invalid --system-overhead-tokens '{val}'", file=sys.stderr)
                i += 1
                continue
        cleaned.append(tok)
        i += 1
    return cleaned


def main():
    from .updater import maybe_auto_update
    maybe_auto_update()

    argv = _prescan_argv(sys.argv[1:])
    parser = build_parser()
    args = parser.parse_args(argv)

    # Also handle --context-window when placed before the subcommand (root parser)
    if getattr(args, "context_window", None):
        os.environ["COZEMPIC_CONTEXT_WINDOW"] = str(args.context_window)
    if args.system_overhead_tokens:
        os.environ["COZEMPIC_SYSTEM_OVERHEAD_TOKENS"] = str(args.system_overhead_tokens)

    if not args.command:
        parser.print_help()
        sys.exit(0)

    commands = {
        "list": cmd_list,
        "current": cmd_current,
        "diagnose": cmd_diagnose,
        "treat": cmd_treat,
        "strategy": cmd_strategy,
        "reload": cmd_reload,
        "checkpoint": cmd_checkpoint,
        "post-compact": cmd_post_compact,
        "guard": cmd_guard,
        "init": cmd_init,
        "doctor": cmd_doctor,
        "formulary": cmd_formulary,
        "completions": cmd_completions,
    }

    commands[args.command](args)


if __name__ == "__main__":
    main()
