#!/usr/bin/env python3
"""Sync Claude Code sessions to Obsidian markdown.

Usage:
    claude-sessions sync [--session-id ID] [--transcript PATH]
    claude-sessions export (--today | --all | FILE)
    claude-sessions resume (--pick | --active | FILE) [--fork]
    claude-sessions note TEXT [--session-id ID]
    claude-sessions close [TEXT] [--session-id ID]
    claude-sessions list [--active | --all] [--json]
"""

import argparse
import json
import os
import re
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

__version__ = "1.0.0"

# Configuration - auto-detect from environment or CWD
def _detect_vault_dir():
    """Auto-detect Obsidian vault from VAULT_DIR env var or by walking up from CWD."""
    if os.environ.get("VAULT_DIR"):
        return Path(os.environ["VAULT_DIR"])
    cwd = Path.cwd()
    for parent in [cwd, *cwd.parents]:
        if (parent / ".obsidian").is_dir():
            return parent
    return cwd

def _detect_sessions_dir():
    """Auto-detect Claude project sessions directory from CWD."""
    cwd = str(Path.cwd())
    encoded = cwd.replace("/", "-")
    candidate = Path.home() / ".claude" / "projects" / encoded
    if candidate.is_dir():
        return candidate
    # Fallback: most recently modified project dir
    projects = Path.home() / ".claude" / "projects"
    if projects.is_dir():
        dirs = sorted([d for d in projects.iterdir() if d.is_dir()], key=lambda d: d.stat().st_mtime, reverse=True)
        if dirs:
            return dirs[0]
    return projects

VAULT_DIR = _detect_vault_dir()
OUTPUT_DIR = VAULT_DIR / "Claude-Sessions"
SKILLS_DIR = VAULT_DIR / ".claude" / "skills"
SESSIONS_DIR = _detect_sessions_dir()

PRESERVED_FIELDS = {"comments", "projects", "status", "tags", "rating", "title"}
PRESERVED_SECTION = "## My Notes"


# =============================================================================
# Parsing & Extraction
# =============================================================================

def parse_jsonl(file_path: Path) -> list[dict]:
    """Parse a JSONL file into a list of records."""
    records = []
    if not file_path.exists():
        return records
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                try:
                    records.append(json.loads(line))
                except json.JSONDecodeError:
                    continue
    return records


def get_skill_relative_path(skill_name: str) -> str | None:
    """Get relative path from Claude-Sessions folder to skill SKILL.md."""
    skill_file = SKILLS_DIR / skill_name / "SKILL.md"
    if skill_file.exists():
        return f"../.claude/skills/{skill_name}/SKILL.md"
    return None


def path_to_wikilink(file_path: str) -> str | None:
    """Convert a file path to a wiki link if it's in the vault."""
    vault_str = str(VAULT_DIR)
    if file_path.startswith(vault_str):
        rel_path = file_path[len(vault_str):].lstrip("/")
        if rel_path.endswith(".md"):
            rel_path = rel_path[:-3]
        return f"[[{rel_path}]]"
    return None


def extract_file_operations(records: list[dict]) -> dict:
    """Extract files created and modified from toolUseResult records."""
    files_created = []
    files_modified = set()

    for record in records:
        result = record.get("toolUseResult", {})
        if not isinstance(result, dict):
            continue
        file_path = result.get("filePath")
        if not file_path:
            continue
        if result.get("type") == "create":
            if file_path not in files_created:
                files_created.append(file_path)
        elif result.get("structuredPatch") or result.get("oldString"):
            files_modified.add(file_path)

    files_modified = files_modified - set(files_created)
    return {"created": files_created, "modified": list(files_modified)}


def extract_session_data(records: list[dict]) -> dict:
    """Extract session metadata from records."""
    data = {
        "session_id": None,
        "date": None,
        "title": None,
        "summary": None,
        "skills": [],
        "messages": 0,
        "user_messages": [],
        "first_timestamp": None,
        "last_timestamp": None,
        "files_created": [],
        "files_modified": [],
    }

    for record in records:
        record_type = record.get("type")

        if record.get("sessionId") and not data["session_id"]:
            data["session_id"] = record["sessionId"]

        if record_type == "user":
            timestamp = record.get("timestamp", "")
            if timestamp:
                if not data["date"]:
                    data["date"] = timestamp.split("T")[0]
                if not data["first_timestamp"]:
                    data["first_timestamp"] = timestamp
                data["last_timestamp"] = timestamp

        if record_type == "custom-title":
            custom_title = record.get("customTitle", "")
            if custom_title:
                data["title"] = custom_title.split("\n")[0].strip()[:100]

        if record_type == "summary":
            summary = record.get("summary", "")
            if summary:
                data["summary"] = summary

        if record_type == "user":
            data["messages"] += 1
            msg = record.get("message", {})
            content = msg.get("content", "")
            if content and isinstance(content, str):
                if not record.get("isMeta"):
                    data["user_messages"].append(content)
            elif isinstance(content, list):
                for item in content:
                    if isinstance(item, dict) and item.get("type") == "text":
                        text = item.get("text", "")
                        if text and not record.get("isMeta"):
                            data["user_messages"].append(text)

        if record_type == "assistant":
            msg = record.get("message", {})
            contents = msg.get("content", [])
            if isinstance(contents, list):
                for item in contents:
                    if isinstance(item, dict) and item.get("name") == "Skill":
                        skill_input = item.get("input", {})
                        skill_name = skill_input.get("skill", "")
                        if skill_name and skill_name not in data["skills"]:
                            data["skills"].append(skill_name)

    if not data["title"] and data["user_messages"]:
        first_msg = data["user_messages"][0]
        data["title"] = first_msg.replace("\n", " ").strip()[:80]

    if not data["date"]:
        data["date"] = datetime.now().strftime("%Y-%m-%d")

    file_ops = extract_file_operations(records)
    data["files_created"] = file_ops["created"]
    data["files_modified"] = file_ops["modified"]

    return data


# =============================================================================
# Frontmatter & Markdown
# =============================================================================

def parse_frontmatter(content: str) -> dict:
    """Parse frontmatter from markdown content."""
    frontmatter = {}
    match = re.match(r"^---\n(.*?)\n---\n", content, re.DOTALL)
    if not match:
        return frontmatter

    fm_text = match.group(1)
    current_key = None
    current_array = None
    multiline_value = []
    in_multiline = False

    for line in fm_text.split("\n"):
        # Handle multiline string (|)
        if in_multiline:
            if line.startswith("  ") or line == "":
                multiline_value.append(line[2:] if line.startswith("  ") else "")
                continue
            else:
                frontmatter[current_key] = "\n".join(multiline_value).rstrip()
                in_multiline = False
                multiline_value = []

        # Array item
        if line.startswith("  - "):
            if current_array:
                if current_array not in frontmatter:
                    frontmatter[current_array] = []
                elif not isinstance(frontmatter[current_array], list):
                    frontmatter[current_array] = []
                frontmatter[current_array].append(line[4:].strip())
            continue

        # Key-value
        if ":" in line and not line.startswith("  "):
            key, value = line.split(":", 1)
            key = key.strip()
            value = value.strip()
            current_key = key

            if value == "|":
                in_multiline = True
                multiline_value = []
                current_array = None
            elif value == "" or value == "[]":
                current_array = key
                frontmatter[key] = [] if value == "[]" else value
            else:
                current_array = None
                if value.startswith('"') and value.endswith('"'):
                    value = value[1:-1]
                frontmatter[key] = value

    if in_multiline:
        frontmatter[current_key] = "\n".join(multiline_value).rstrip()

    return frontmatter


def extract_my_notes_section(content: str) -> str | None:
    """Extract the '## My Notes' section from existing content."""
    if PRESERVED_SECTION not in content:
        return None
    pattern = rf"({re.escape(PRESERVED_SECTION)}.*?)(?=\n## |\Z)"
    match = re.search(pattern, content, re.DOTALL)
    if match:
        return match.group(1).rstrip()
    return None


def generate_markdown(data: dict, session_id: str, existing_fm: dict | None = None, my_notes: str | None = None) -> str:
    """Generate markdown content from session data."""
    lines = []

    # Frontmatter
    lines.append("---")
    lines.append("type: claude-session")
    lines.append(f"date: {data['date']}")
    lines.append(f"session_id: {session_id}")

    # Use preserved title if exists, otherwise from transcript
    title = existing_fm.get("title") if existing_fm else None
    if not title or title == "Untitled Session":
        title = data["title"] or "Untitled Session"
    title_escaped = title.replace('"', '\\"')
    lines.append(f'title: "{title_escaped}"')

    if data["summary"]:
        summary_escaped = data["summary"].replace('"', '\\"').replace("\n", " ")
        lines.append(f'summary: "{summary_escaped}"')

    if data["skills"]:
        lines.append("skills:")
        for skill in sorted(data["skills"]):
            lines.append(f"  - {skill}")

    lines.append(f"messages: {data['messages']}")

    last_activity = data.get("last_timestamp") or datetime.now(timezone.utc).isoformat()
    lines.append(f"last_activity: {last_activity}")

    # Preserved fields
    status = existing_fm.get("status", "active") if existing_fm else "active"
    lines.append(f"status: {status}")

    # Tags
    tags = existing_fm.get("tags", []) if existing_fm else []
    if isinstance(tags, list) and tags:
        lines.append("tags:")
        for tag in tags:
            lines.append(f"  - {tag}")
    else:
        lines.append("tags: []")

    # Rating
    rating = existing_fm.get("rating") if existing_fm else None
    if rating is not None and rating != "null":
        lines.append(f"rating: {rating}")
    else:
        lines.append("rating: null")

    # Comments (timestamped entries)
    comments = existing_fm.get("comments", "") if existing_fm else ""
    if comments:
        lines.append("comments: |")
        for comment_line in comments.split("\n"):
            lines.append(f"  {comment_line}")
    else:
        lines.append('comments: ""')

    # Projects
    projects = existing_fm.get("projects", []) if existing_fm else []
    if isinstance(projects, list) and projects:
        lines.append("projects:")
        for proj in projects:
            lines.append(f"  - {proj}")
    else:
        lines.append("projects: []")

    lines.append("---")
    lines.append("")

    # Content
    lines.append(f"# {title}")
    lines.append("")

    if data["summary"]:
        lines.append("## Summary")
        lines.append("")
        lines.append(data["summary"])
        lines.append("")

    if data["skills"]:
        lines.append("## Skills Used")
        lines.append("")
        for skill in sorted(data["skills"]):
            skill_path = get_skill_relative_path(skill)
            if skill_path:
                lines.append(f"- [{skill}]({skill_path})")
            else:
                lines.append(f"- {skill}")
        lines.append("")

    # Artifacts
    files_created = data.get("files_created", [])
    files_modified = data.get("files_modified", [])

    if files_created or files_modified:
        lines.append("## Artifacts")
        lines.append("")
        if files_created:
            lines.append("**Created:**")
            for file_path in files_created:
                wikilink = path_to_wikilink(file_path)
                lines.append(f"- {wikilink}" if wikilink else f"- `{file_path}`")
            lines.append("")
        if files_modified:
            lines.append("**Modified:**")
            for file_path in sorted(files_modified):
                wikilink = path_to_wikilink(file_path)
                lines.append(f"- {wikilink}" if wikilink else f"- `{file_path}`")
            lines.append("")

    # My Notes
    if my_notes:
        lines.append(my_notes)
    else:
        lines.append("## My Notes")
        lines.append("")
        lines.append("<!-- Add your notes here. This section is preserved across syncs. -->")
    lines.append("")

    # Conversation
    lines.append("## Conversation")
    lines.append("")
    for msg in data["user_messages"]:
        lines.append("### User")
        lines.append("")
        lines.append(msg)
        lines.append("")

    return "\n".join(lines)


# =============================================================================
# File Operations
# =============================================================================

def find_session_file(session_id: str) -> Path | None:
    """Find existing markdown file for a session."""
    short_id = session_id[:8]
    for f in OUTPUT_DIR.glob(f"*-{short_id}.md"):
        return f
    return None


def get_output_path(session_id: str, date: str) -> Path:
    """Get output file path for a session."""
    return OUTPUT_DIR / f"{date}-{session_id[:8]}.md"


def sync_session(session_id: str, transcript_path: str | None = None, quiet: bool = False) -> Path | None:
    """Sync a session to markdown. Returns output path or None."""
    if session_id.startswith("agent-"):
        return None

    if transcript_path:
        jsonl_path = Path(transcript_path)
    else:
        jsonl_path = SESSIONS_DIR / f"{session_id}.jsonl"

    if not jsonl_path.exists() or jsonl_path.stat().st_size == 0:
        return None

    records = parse_jsonl(jsonl_path)
    if not records:
        return None

    data = extract_session_data(records)
    data["session_id"] = session_id

    existing_file = find_session_file(session_id)
    output_file = existing_file or get_output_path(session_id, data["date"])

    existing_fm = None
    my_notes = None
    if output_file.exists():
        content = output_file.read_text(encoding="utf-8")
        existing_fm = parse_frontmatter(content)
        my_notes = extract_my_notes_section(content)

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    markdown = generate_markdown(data, session_id, existing_fm, my_notes)
    output_file.write_text(markdown, encoding="utf-8")

    if not quiet:
        print(f"Synced: {output_file}")

    return output_file


def add_comment(session_id: str, text: str) -> bool:
    """Add a timestamped comment to a session."""
    session_file = find_session_file(session_id)
    if not session_file or not session_file.exists():
        print(f"Error: Session file not found for {session_id[:8]}", file=sys.stderr)
        return False

    content = session_file.read_text(encoding="utf-8")
    fm = parse_frontmatter(content)

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
    new_entry = f"[{timestamp}] {text}"

    existing_comments = fm.get("comments", "")
    if existing_comments:
        new_comments = f"{existing_comments}\n{new_entry}"
    else:
        new_comments = new_entry

    # Update frontmatter - replace comments field
    comments_yaml = "comments: |\n" + "\n".join(f"  {line}" for line in new_comments.split("\n"))

    if 'comments: |' in content:
        # Replace existing multiline comments
        pattern = r'comments: \|\n(?:  .*\n)*'
        new_content = re.sub(pattern, comments_yaml + "\n", content)
    elif 'comments: ""' in content:
        # Replace empty comments
        new_content = content.replace('comments: ""', comments_yaml)
    else:
        # Add comments field before projects
        new_content = content.replace("projects:", f"{comments_yaml}\nprojects:")

    session_file.write_text(new_content, encoding="utf-8")
    print(f"Added comment: {new_entry}")
    return True


def set_session_status(session_id: str, status: str) -> bool:
    """Set session status in frontmatter."""
    session_file = find_session_file(session_id)
    if not session_file or not session_file.exists():
        print(f"Error: Session file not found for {session_id[:8]}", file=sys.stderr)
        return False

    content = session_file.read_text(encoding="utf-8")
    new_content = re.sub(r"status: \w+", f"status: {status}", content)
    session_file.write_text(new_content, encoding="utf-8")
    return True


def set_session_rating(session_id: str, rating: int) -> bool:
    """Set session rating in frontmatter."""
    session_file = find_session_file(session_id)
    if not session_file or not session_file.exists():
        print(f"Error: Session file not found for {session_id[:8]}", file=sys.stderr)
        return False

    if not 1 <= rating <= 10:
        print(f"Error: Rating must be 1-10, got {rating}", file=sys.stderr)
        return False

    content = session_file.read_text(encoding="utf-8")
    new_content = re.sub(r"rating: (null|\d+)", f"rating: {rating}", content)
    session_file.write_text(new_content, encoding="utf-8")
    return True


def set_session_tags(session_id: str, tags: list[str]) -> bool:
    """Set session tags in frontmatter."""
    session_file = find_session_file(session_id)
    if not session_file or not session_file.exists():
        print(f"Error: Session file not found for {session_id[:8]}", file=sys.stderr)
        return False

    content = session_file.read_text(encoding="utf-8")

    # Build new tags yaml
    if tags:
        tags_yaml = "tags:\n" + "\n".join(f"  - {tag}" for tag in tags)
    else:
        tags_yaml = "tags: []"

    # Replace existing tags section
    if "tags:" in content:
        # Handle both array and empty tags
        pattern = r"tags:(?:\s*\[\]|\n(?:  - .*\n)*)"
        new_content = re.sub(pattern, tags_yaml + "\n", content)
    else:
        # Add before rating
        new_content = content.replace("rating:", f"{tags_yaml}\nrating:")

    session_file.write_text(new_content, encoding="utf-8")
    return True


# =============================================================================
# Session Listing
# =============================================================================

def get_session_files() -> list[tuple[Path, dict]]:
    """Get all session files with frontmatter, sorted by last activity."""
    sessions = []
    for f in OUTPUT_DIR.glob("*.md"):
        try:
            content = f.read_text(encoding="utf-8")
            fm = parse_frontmatter(content)
            if fm.get("type") == "claude-session":
                sessions.append((f, fm))
        except Exception:
            continue

    return sorted(sessions, key=lambda x: x[1].get("last_activity", x[1].get("date", "")), reverse=True)


def get_active_sessions() -> list[tuple[Path, dict]]:
    """Get sessions with status 'active'."""
    return [(f, fm) for f, fm in get_session_files() if fm.get("status") == "active"]


def print_sessions(sessions: list[tuple[Path, dict]], title: str = "Sessions"):
    """Print formatted session list."""
    print(f"\n{title}:")
    print("-" * 80)
    if not sessions:
        print("  No sessions found.")
        return
    for i, (path, fm) in enumerate(sessions[:20], 1):
        t = fm.get("title", "Untitled")[:50]
        s = fm.get("status", "?")
        m = fm.get("messages", "?")
        d = fm.get("date", "?")
        print(f"  {i:2}. [{s:8}] {d} ({m:>3} msgs) {t}")
    if len(sessions) > 20:
        print(f"  ... and {len(sessions) - 20} more")


def interactive_pick(sessions: list[tuple[Path, dict]]) -> Path | None:
    """Interactive session picker."""
    if not sessions:
        print("No sessions to pick from.")
        return None

    lines = []
    for path, fm in sessions:
        t = fm.get("title", "Untitled")[:60]
        s = fm.get("status", "?")
        d = fm.get("date", "?")
        m = fm.get("messages", "?")
        lines.append(f"{path}\t[{s}] {d} ({m} msgs) {t}")

    try:
        result = subprocess.run(
            ["fzf", "--delimiter=\t", "--with-nth=2", "--preview=head -50 {1}"],
            input="\n".join(lines),
            capture_output=True,
            text=True,
        )
        if result.returncode == 0 and result.stdout.strip():
            return Path(result.stdout.strip().split("\t")[0])
    except FileNotFoundError:
        print_sessions(sessions, "Pick a session")
        try:
            choice = input("\nEnter number (or q to quit): ").strip()
            if choice.lower() == "q":
                return None
            idx = int(choice) - 1
            if 0 <= idx < len(sessions):
                return sessions[idx][0]
        except (ValueError, KeyboardInterrupt):
            return None
    return None


# =============================================================================
# Commands
# =============================================================================

def cmd_sync(args):
    """Sync command - from stdin JSON or explicit args."""
    session_id = args.session_id
    transcript_path = args.transcript

    if not session_id:
        # Try reading from stdin (hook mode)
        try:
            hook_input = json.loads(sys.stdin.read())
            session_id = hook_input.get("session_id")
            transcript_path = transcript_path or hook_input.get("transcript_path")
        except (json.JSONDecodeError, Exception):
            pass

    if not session_id:
        # No session_id is normal on first prompt or when stdin is unavailable
        return 0

    result = sync_session(session_id, transcript_path, quiet=args.quiet)
    return 0


def cmd_export(args):
    """Export command - batch export sessions."""
    if args.today:
        cutoff = time.time() - 86400
        sessions = [f for f in SESSIONS_DIR.glob("*.jsonl")
                    if f.stat().st_mtime >= cutoff and f.stat().st_size > 0]
        print(f"Found {len(sessions)} sessions from today")
    elif args.all:
        sessions = [f for f in SESSIONS_DIR.glob("*.jsonl") if f.stat().st_size > 0]
        print(f"Found {len(sessions)} total sessions")
    else:
        sessions = [Path(args.file)]

    for session_file in sessions:
        session_id = session_file.stem
        sync_session(session_id, str(session_file), quiet=args.quiet)

    return 0


def cmd_resume(args):
    """Resume command - resume a session in Claude Code."""
    target_file = None

    if args.file:
        target_file = Path(args.file).expanduser()
        if not target_file.exists():
            print(f"Error: File not found: {target_file}", file=sys.stderr)
            return 1
    elif args.active:
        active = get_active_sessions()
        if not active:
            print("No active sessions found.")
            return 1
        target_file = active[0][0]
        print(f"Most recent active: {target_file.name}")
    elif args.pick:
        sessions = get_session_files() if args.all else get_active_sessions()
        target_file = interactive_pick(sessions)
        if not target_file:
            print("No session selected.")
            return 0
    else:
        print("Error: Specify --pick, --active, or a file", file=sys.stderr)
        return 2

    content = target_file.read_text(encoding="utf-8")
    fm = parse_frontmatter(content)
    session_id = fm.get("session_id")

    if not session_id:
        print(f"Error: No session_id in {target_file}", file=sys.stderr)
        return 1

    cmd = ["claude", "--resume", session_id]
    if args.fork:
        cmd.append("--fork-session")

    print(f"Resuming: {session_id}")
    os.execvp("claude", cmd)


def cmd_note(args):
    """Note command - add timestamped note to session."""
    session_id = args.session_id or os.environ.get("CLAUDE_SESSION_ID")

    if not session_id:
        # Try current session from most recent active
        active = get_active_sessions()
        if active:
            fm = active[0][1]
            session_id = fm.get("session_id")

    if not session_id:
        print("Error: No session_id. Use --session-id or set CLAUDE_SESSION_ID", file=sys.stderr)
        return 1

    text = " ".join(args.text)
    if not text:
        print("Error: No note text provided", file=sys.stderr)
        return 2

    return 0 if add_comment(session_id, text) else 1


def cmd_close(args):
    """Close command - mark session done with optional note."""
    session_id = args.session_id or os.environ.get("CLAUDE_SESSION_ID")

    if not session_id:
        active = get_active_sessions()
        if active:
            fm = active[0][1]
            session_id = fm.get("session_id")

    if not session_id:
        print("Error: No session_id", file=sys.stderr)
        return 1

    if args.text:
        text = " ".join(args.text)
        add_comment(session_id, f"[CLOSED] {text}")
    else:
        add_comment(session_id, "[CLOSED]")

    set_session_status(session_id, "done")
    print(f"Session {session_id[:8]} marked as done")
    return 0


def cmd_log(args):
    """Log command - annotate session with status, tags, rating, and comment."""
    session_id = args.session_id or os.environ.get("CLAUDE_SESSION_ID")

    if not session_id:
        active = get_active_sessions()
        if active:
            fm = active[0][1]
            session_id = fm.get("session_id")

    if not session_id:
        print("Error: No session_id. Use --session-id or set CLAUDE_SESSION_ID", file=sys.stderr)
        return 1

    # Set status if provided
    if args.status:
        valid_statuses = ["active", "done", "blocked", "handoff"]
        if args.status not in valid_statuses:
            print(f"Error: Invalid status '{args.status}'. Must be one of: {', '.join(valid_statuses)}", file=sys.stderr)
            return 1
        set_session_status(session_id, args.status)
        print(f"Status: {args.status}")

    # Set tags if provided
    if args.tags:
        tags = [t.strip() for t in args.tags.split(",")]
        set_session_tags(session_id, tags)
        print(f"Tags: {', '.join(tags)}")

    # Set rating if provided
    if args.rating is not None:
        set_session_rating(session_id, args.rating)
        print(f"Rating: {args.rating}/10")

    # Add comment if provided
    if args.text:
        text = " ".join(args.text)
        add_comment(session_id, text)

    print(f"Session {session_id[:8]} updated")
    return 0


def cmd_list(args):
    """List command - list sessions."""
    if args.all:
        sessions = get_session_files()
        title = "All Sessions"
    else:
        sessions = get_active_sessions()
        title = "Active Sessions"

    if args.json:
        data = [{"path": str(p), **fm} for p, fm in sessions]
        print(json.dumps(data, indent=2))
    else:
        print_sessions(sessions, title)

    return 0


# =============================================================================
# Main
# =============================================================================

def main():
    parser = argparse.ArgumentParser(
        prog="claude-sessions",
        description="Sync Claude Code sessions to Obsidian markdown",
    )
    parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
    parser.add_argument("-q", "--quiet", action="store_true", help="Suppress output")

    subparsers = parser.add_subparsers(dest="command", required=True)

    # sync
    p_sync = subparsers.add_parser("sync", help="Sync session (hook or explicit)")
    p_sync.add_argument("--session-id", help="Session ID")
    p_sync.add_argument("--transcript", help="Transcript file path")
    p_sync.set_defaults(func=cmd_sync)

    # export
    p_export = subparsers.add_parser("export", help="Batch export sessions")
    p_export.add_argument("--today", action="store_true", help="Export today's sessions")
    p_export.add_argument("--all", action="store_true", help="Export all sessions")
    p_export.add_argument("file", nargs="?", help="Specific session file")
    p_export.set_defaults(func=cmd_export)

    # resume
    p_resume = subparsers.add_parser("resume", help="Resume a session")
    p_resume.add_argument("--pick", "-p", action="store_true", help="Interactive picker")
    p_resume.add_argument("--active", "-a", action="store_true", help="Most recent active")
    p_resume.add_argument("--fork", "-f", action="store_true", help="Fork instead of continue")
    p_resume.add_argument("--all", action="store_true", help="Show all (not just active)")
    p_resume.add_argument("file", nargs="?", help="Markdown file to resume from")
    p_resume.set_defaults(func=cmd_resume)

    # note
    p_note = subparsers.add_parser("note", help="Add timestamped note")
    p_note.add_argument("text", nargs="+", help="Note text")
    p_note.add_argument("--session-id", help="Session ID (defaults to current)")
    p_note.set_defaults(func=cmd_note)

    # close
    p_close = subparsers.add_parser("close", help="Mark session done")
    p_close.add_argument("text", nargs="*", help="Optional closing note")
    p_close.add_argument("--session-id", help="Session ID (defaults to current)")
    p_close.set_defaults(func=cmd_close)

    # log
    p_log = subparsers.add_parser("log", help="Annotate session with status/tags/rating")
    p_log.add_argument("text", nargs="*", help="Optional comment")
    p_log.add_argument("--status", "-s", help="Set status (active|done|blocked|handoff)")
    p_log.add_argument("--tags", "-t", help="Set tags (comma-separated)")
    p_log.add_argument("--rating", "-r", type=int, help="Set rating (1-10)")
    p_log.add_argument("--session-id", help="Session ID (defaults to current)")
    p_log.set_defaults(func=cmd_log)

    # list
    p_list = subparsers.add_parser("list", help="List sessions")
    p_list.add_argument("--active", action="store_true", help="Active only (default)")
    p_list.add_argument("--all", action="store_true", help="All sessions")
    p_list.add_argument("--json", action="store_true", help="JSON output")
    p_list.set_defaults(func=cmd_list)

    args = parser.parse_args()
    sys.exit(args.func(args))


if __name__ == "__main__":
    main()
