"""
ShelfAI CLI — Command-line interface for managing your shelf.

Commands:
    init        Initialize a new shelf
    add         Add a file or URL to the shelf
    index       Build/rebuild the index (generate abstracts)
    session     Run the session management agent on a transcript
    search      Search the index by keyword
    status      Show shelf health and statistics
    export      Export the shelf as a single file
    prune       Clean up stale memory entries
    chunk-scan  Scan agents directory for chunking candidates
    chunk       Run heuristic pre-filter on a monolithic agent file
    compact     Consolidate memory files (remove duplicates, archive stale)
"""

import shlex
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console
from rich.table import Table

from shelfai import __version__

app = typer.Typer(
    name="shelfai",
    help="📚 ShelfAI — Lightweight, file-based context management for AI agents.",
    no_args_is_help=True,
)
console = Console()

_PROVIDER_DEFAULTS = {
    "anthropic": {
        "model": "claude-sonnet-4-20250514",
        "api_key_env": "ANTHROPIC_API_KEY",
    },
    "openai": {
        "model": "gpt-4o",
        "api_key_env": "OPENAI_API_KEY",
    },
    "gpt": {
        "model": "gpt-4o",
        "api_key_env": "OPENAI_API_KEY",
    },
}


def _apply_provider_overrides(config, provider: Optional[str], model: Optional[str]) -> None:
    previous_provider = config.session.provider.lower()
    selected_provider = (provider or previous_provider).lower()
    defaults = _PROVIDER_DEFAULTS.get(selected_provider, {})

    config.session.provider = selected_provider
    if defaults.get("api_key_env"):
        config.session.api_key_env = defaults["api_key_env"]
    if model is not None:
        config.session.model = model
    elif provider and selected_provider != previous_provider and defaults.get("model"):
        config.session.model = defaults["model"]


# ──────────────────────────────────────────────
# shelfai init
# ──────────────────────────────────────────────


@app.command()
def init(
    path: str = typer.Option("./shelf", "--path", "-p", help="Directory to initialize"),
    template: str = typer.Option("minimal", "--template", "-t", help="Starter template: minimal, agent, research"),
):
    """Initialize a new ShelfAI shelf."""
    from shelfai.core.config import ShelfConfig

    shelf_path = Path(path).resolve()

    if (shelf_path / "index.md").exists():
        console.print(f"[yellow]Shelf already exists at {shelf_path}[/yellow]")
        raise typer.Exit(1)

    # Create directory structure
    shelf_path.mkdir(parents=True, exist_ok=True)
    for subdir in ["skills", "knowledge", "resources"]:
        (shelf_path / subdir).mkdir(exist_ok=True)
    (shelf_path / "memory" / "user").mkdir(parents=True, exist_ok=True)
    (shelf_path / "memory" / "agent").mkdir(parents=True, exist_ok=True)

    # Write config
    config = ShelfConfig.default()
    (shelf_path / "shelf.config.yaml").write_text(config.to_yaml(), encoding="utf-8")

    # Write index
    index_content = _get_template_index(template)
    (shelf_path / "index.md").write_text(index_content, encoding="utf-8")

    # Write starter memory files
    (shelf_path / "memory" / "user" / "preferences.md").write_text(
        "---\ntitle: User Preferences\ntype: memory\n---\n\n# User Preferences\n\n"
        "<!-- The session manager will add entries here automatically. -->\n"
        "<!-- You can also add preferences manually. -->\n",
        encoding="utf-8",
    )
    (shelf_path / "memory" / "agent" / "lessons.md").write_text(
        "---\ntitle: Agent Lessons\ntype: memory\n---\n\n# Agent Lessons\n\n"
        "<!-- The session manager will add entries here automatically. -->\n",
        encoding="utf-8",
    )
    (shelf_path / "memory" / "agent" / "patterns.md").write_text(
        "---\ntitle: Task Patterns\ntype: memory\n---\n\n# Task Patterns\n\n"
        "<!-- The session manager will add entries here automatically. -->\n",
        encoding="utf-8",
    )

    # Write .gitkeep files so empty dirs are tracked
    for subdir in ["skills", "knowledge", "resources", "generated/review/new_context"]:
        (shelf_path / subdir).mkdir(parents=True, exist_ok=True)
        (shelf_path / subdir / ".gitkeep").touch()

    console.print(f"[green]Shelf initialized at {shelf_path}[/green]\n")
    console.print("  Created shelf/index.md")
    console.print("  Created shelf/shelf.config.yaml")
    console.print("  Created shelf/skills/")
    console.print("  Created shelf/knowledge/")
    console.print("  Created shelf/memory/user/")
    console.print("  Created shelf/memory/agent/")
    console.print("  Created shelf/resources/")
    console.print("  Created shelf/generated/review/new_context/")
    console.print()
    console.print("Next steps:")
    console.print("  1. Add files:      shelfai add ./my_doc.md --category knowledge")
    console.print("  2. Write abstracts: Edit shelf/index.md")
    console.print("  3. Or auto-index:   shelfai index")


# ──────────────────────────────────────────────
# shelfai add
# ──────────────────────────────────────────────


@app.command()
def add(
    source: str = typer.Argument(..., help="File path or URL to add"),
    category: str = typer.Option(
        "resources", "--category", "-c", help="Target category: skills, knowledge, memory, resources"
    ),
    name: Optional[str] = typer.Option(None, "--name", "-n", help="Override filename"),
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """Add a file or URL to the shelf."""
    from shelfai.core.shelf import Shelf
    from shelfai.core.safety import validate_path

    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
        raise typer.Exit(1)

    config = shelf.load_config()
    allowed_categories = set(config.categories or [])
    safe_category = category.strip().replace("\\", "/").strip("/")

    if (
        not safe_category
        or "/" in safe_category
        or safe_category not in allowed_categories
    ):
        console.print(
            f"[red]Invalid category: {category}. Allowed: {', '.join(sorted(allowed_categories))}[/red]"
        )
        raise typer.Exit(1)

    source_path = Path(source)

    # Determine target filename
    if name:
        target_name = name if name.endswith(".md") else f"{name}.md"
    elif source_path.exists():
        target_name = source_path.name
    elif source.startswith("http"):
        # Derive from URL
        from urllib.parse import urlparse

        parsed = urlparse(source)
        slug = parsed.path.strip("/").split("/")[-1] or "page"
        slug = slug.replace(".html", "").replace(".htm", "")
        target_name = f"{slug}.md"
    else:
        console.print(f"[red]Source not found: {source}[/red]")
        raise typer.Exit(1)

    # Sanitize the filename to prevent path traversal
    target_name = Path(target_name).name
    if target_name in {"", ".", ".."}:
        console.print(f"[red]Invalid target name: {target_name!r}[/red]")
        raise typer.Exit(1)

    relative_path = f"{safe_category}/{target_name}"
    validate_path(shelf.path, relative_path)
    full_target = shelf.path / relative_path

    if full_target.exists():
        console.print(f"[yellow]File already exists: {relative_path}[/yellow]")
        overwrite = typer.confirm("Overwrite?", default=False)
        if not overwrite:
            raise typer.Exit(0)

    # Copy or fetch the content
    if source_path.exists():
        full_target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source_path, full_target)
        console.print(f"[green]Added {relative_path}[/green]")
    elif source.startswith("http"):
        console.print(f"[dim]Fetching {source}...[/dim]")
        try:
            import urllib.request

            request = urllib.request.Request(
                source, headers={"User-Agent": f"ShelfAI/{__version__}"}
            )
            response = urllib.request.urlopen(request)
            content_type = response.headers.get("Content-Type", "")
            content = response.read().decode("utf-8", errors="replace")

            if "text/markdown" in content_type or source.endswith(".md"):
                relative_path = f"{safe_category}/{Path(target_name).stem}.md"
                full_target = validate_path(shelf.path, relative_path)
                full_target.parent.mkdir(parents=True, exist_ok=True)
                full_target.write_text(content, encoding="utf-8")
                console.print(f"[green]Fetched and saved {relative_path}[/green]")
            elif "text/html" in content_type or ("text/" in content_type and "html" in content.lower()[:512]):
                relative_path = f"resources/raw/{Path(target_name).stem}.html"
                full_target = validate_path(shelf.path, relative_path)
                full_target.parent.mkdir(parents=True, exist_ok=True)
                full_target.write_text(content, encoding="utf-8")
                console.print(
                    f"[yellow]Fetched HTML and saved raw source to {relative_path}[/yellow]"
                )
                console.print(
                    "[dim]Raw HTML is kept out of the markdown protocol. Normalize it before indexing.[/dim]"
                )
            elif content_type.startswith("text/"):
                relative_path = f"{safe_category}/{Path(target_name).stem}.md"
                full_target = validate_path(shelf.path, relative_path)
                full_target.parent.mkdir(parents=True, exist_ok=True)
                full_target.write_text(content, encoding="utf-8")
                console.print(f"[green]Fetched and saved {relative_path}[/green]")
            else:
                raise ValueError(
                    f"Unsupported remote content type '{content_type}'. "
                    "Only text/markdown, text/plain, and text/html are supported."
                )
        except Exception as e:
            console.print(f"[red]Failed to fetch URL: {e}[/red]")
            raise typer.Exit(1)
    else:
        console.print(f"[red]Source not found: {source}[/red]")
        raise typer.Exit(1)

    console.print("Run [bold]shelfai index[/bold] to update the index with an abstract.")


# ──────────────────────────────────────────────
# shelfai index
# ──────────────────────────────────────────────


@app.command(name="index")
def index_cmd(
    all_files: bool = typer.Option(False, "--all", help="Regenerate all abstracts"),
    dry_run: bool = typer.Option(False, "--dry-run", help="Show what would change"),
    manual: bool = typer.Option(False, "--manual", help="Open index.md in editor"),
    context: Optional[str] = typer.Option(None, "--context", help="Path to context file for domain-aware abstracts"),
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """Build or rebuild the shelf index. Generates abstracts for new/modified files."""
    import os

    from shelfai.core.shelf import Shelf

    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
        raise typer.Exit(1)

    if manual:
        editor = os.environ.get("EDITOR", "nano")
        index_path = Path(shelf_path) / "index.md"
        subprocess.run(shlex.split(editor) + [str(index_path)], check=False)
        return

    # Load context file if provided
    context_text = None
    if context:
        context_path = Path(context)
        if context_path.exists():
            context_text = context_path.read_text(encoding="utf-8")
        else:
            console.print(f"[red]Context file not found: {context}[/red]")
            raise typer.Exit(1)

    # Check for unindexed files that could be indexed manually
    files = shelf.list_files()
    index = shelf.index
    unindexed = index.unindexed_files(files)
    stale = index.stale_entries(files)

    if not unindexed and not stale and not all_files:
        console.print("[green]Index is up to date. All files indexed.[/green]")
        return

    # Try to use LLM for auto-indexing
    try:
        config = shelf.load_config()
        from shelfai.providers import get_provider
        from shelfai.agents.indexer import IndexerAgent

        provider = get_provider(config)
        indexer = IndexerAgent(shelf, provider)

        with console.status("Generating abstracts..."):
            result = indexer.index_all(context=context_text, force=all_files)

        # Display results
        for item in result["generated"]:
            status = "[new]" if not all_files else "[regen]"
            console.print(f"  [green]{status}[/green] {item['file']}")
            console.print(f"         [dim]{item['abstract']}[/dim]")

        for file_path in result["unchanged"]:
            console.print(f"  [dim][ok][/dim]    {file_path}")

        for error in result["errors"]:
            console.print(f"  [red][err][/red]   {error['file']}: {error['error']}")

        # Remove stale entries
        for entry in stale:
            try:
                index.remove(entry.file_path)
                console.print(f"  [yellow][removed][/yellow] {entry.file_path} (file no longer exists)")
            except KeyError:
                pass

        gen_count = len(result["generated"])
        unchanged_count = len(result["unchanged"])
        console.print(
            f"\n[bold]Index updated: {gen_count} generated, {unchanged_count} unchanged.[/bold]"
        )

    except (ImportError, ValueError) as e:
        # No LLM provider available — show manual indexing info
        console.print(
            f"[yellow]No LLM provider configured ({e}).[/yellow]\n"
            f"You can:\n"
            f"  1. Write abstracts manually: [bold]shelfai index --manual[/bold]\n"
            f"  2. Configure a provider in shelf.config.yaml\n"
            f"  3. Install a provider: [bold]pip install shelfai[anthropic][/bold]"
        )

        if unindexed:
            console.print(f"\nUnindexed files ({len(unindexed)}):")
            for f in unindexed:
                console.print(f"  - {f}")


# ──────────────────────────────────────────────
# shelfai session
# ──────────────────────────────────────────────


@app.command()
def session(
    transcript: Optional[str] = typer.Argument(None, help="Path to transcript file"),
    stdin: bool = typer.Option(False, "--stdin", help="Read transcript from stdin"),
    dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes without applying"),
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """Run the session management agent on a conversation transcript."""
    from shelfai.core.shelf import Shelf
    from shelfai.agents.session import SessionManager
    from shelfai.providers import get_provider

    # Read transcript
    if stdin:
        content = sys.stdin.read()
        if not content.strip():
            console.print("[red]No input received from stdin.[/red]")
            raise typer.Exit(1)
    elif transcript:
        path = Path(transcript)
        if not path.exists():
            console.print(f"[red]File not found: {transcript}[/red]")
            raise typer.Exit(1)
        content = path.read_text(encoding="utf-8")
    else:
        console.print("[red]Provide a transcript file or use --stdin.[/red]")
        raise typer.Exit(1)

    # Initialize
    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
        raise typer.Exit(1)

    config = shelf.load_config()

    try:
        provider = get_provider(config)
    except (ImportError, ValueError) as e:
        console.print(f"[red]LLM provider error: {e}[/red]")
        console.print("Configure a provider in shelf.config.yaml and install it:")
        console.print("  pip install shelfai[anthropic]  # or shelfai[openai]")
        raise typer.Exit(1)

    manager = SessionManager(shelf, provider)

    # Process
    with console.status("Analyzing session..."):
        report = manager.process_text(content, dry_run=dry_run)

    # Display
    _display_session_report(report)


# ──────────────────────────────────────────────
# shelfai search
# ──────────────────────────────────────────────


@app.command()
def search(
    query: str = typer.Argument(..., help="Search query"),
    top: int = typer.Option(5, "--top", "-n", help="Number of results"),
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """Search the index for matching abstracts."""
    from shelfai.core.shelf import Shelf

    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
        raise typer.Exit(1)

    results = shelf.index.search(query, top_n=top)

    if not results:
        console.print(f"[dim]No matches found for: {query}[/dim]")
        return

    console.print(f"\n[bold]Results for:[/bold] {query}\n")
    for i, entry in enumerate(results, 1):
        console.print(f"  [bold]{i}.[/bold] [cyan]{entry.file_path}[/cyan]")
        console.print(f"     {entry.abstract}")
        console.print()


# ──────────────────────────────────────────────
# shelfai status
# ──────────────────────────────────────────────


@app.command()
def status(
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """Show shelf health and statistics."""
    from shelfai.core.shelf import Shelf

    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
        raise typer.Exit(1)

    stats = shelf.status()

    console.print("\n[bold]📚 ShelfAI Status[/bold]\n")

    table = Table(show_header=False, box=None, padding=(0, 2))
    table.add_column(style="dim", min_width=20)
    table.add_column()

    table.add_row("Shelf path", str(shelf.path))
    table.add_row("Total files", str(stats["total_files"]))
    table.add_row("Indexed", f"{stats['indexed_count']}/{stats['total_files']}")
    table.add_row("Index tokens", f"~{stats['index_tokens']:,}")
    table.add_row("Total tokens", f"~{stats['estimated_tokens']:,}")

    console.print(table)

    # Category breakdown
    if stats["categories"]:
        console.print("\n[bold]Categories:[/bold]")
        for cat, count in stats["categories"].items():
            console.print(f"  {cat}: {count} files")

    # Warnings
    if stats["unindexed"]:
        console.print(f"\n[yellow]Unindexed files ({len(stats['unindexed'])}):[/yellow]")
        for f in stats["unindexed"]:
            console.print(f"  - {f}")
        console.print("  Run [bold]shelfai index[/bold] to generate abstracts.")

    if stats["stale"]:
        console.print(f"\n[yellow]Stale index entries ({len(stats['stale'])}):[/yellow]")
        for f in stats["stale"]:
            console.print(f"  - {f}")
        console.print("  Run [bold]shelfai index[/bold] to clean up.")

    console.print()


# ──────────────────────────────────────────────
# shelfai export
# ──────────────────────────────────────────────


@app.command()
def export(
    categories: Optional[str] = typer.Option(None, "--categories", help="Comma-separated categories"),
    index_only: bool = typer.Option(False, "--index-only", help="Export only the index"),
    max_tokens: Optional[int] = typer.Option(None, "--max-tokens", help="Token budget"),
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """Export the shelf as a single file (prints to stdout)."""
    from shelfai.core.shelf import Shelf
    from shelfai.core.fileops import estimate_tokens

    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]", err=True)
        raise typer.Exit(1)

    cat_list = categories.split(",") if categories else None

    result = shelf.export(
        categories=cat_list,
        index_only=index_only,
        max_tokens=max_tokens,
    )

    # Print to stdout (not rich console, so it can be piped)
    print(result)

    # Print stats to stderr
    token_count = estimate_tokens(result)
    console.print(f"[dim]Exported (~{token_count:,} tokens)[/dim]", err=True)


# ──────────────────────────────────────────────
# shelfai prune
# ──────────────────────────────────────────────


@app.command()
def prune(
    age: Optional[int] = typer.Option(None, "--age", help="Only flag entries older than N days"),
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """Clean up stale memory entries and index references."""
    from shelfai.core.shelf import Shelf

    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
        raise typer.Exit(1)

    stats = shelf.status()

    changes = False

    # Clean stale index entries
    if stats["stale"]:
        console.print("\n[yellow]Stale index entries (files no longer exist):[/yellow]")
        for f in stats["stale"]:
            console.print(f"  - {f}")

        if typer.confirm("Remove these stale index entries?", default=True):
            for f in stats["stale"]:
                try:
                    shelf.index.remove(f)
                except KeyError:
                    pass
            console.print(f"[green]Removed {len(stats['stale'])} stale entries.[/green]")
            changes = True

    # Check for pending corrections
    corrections_path = shelf.path / "memory" / "agent" / "corrections_pending.md"
    if corrections_path.exists():
        content = corrections_path.read_text(encoding="utf-8")
        if content.strip() and "##" in content:
            console.print("\n[yellow]Pending knowledge corrections to review:[/yellow]")
            console.print(f"  {corrections_path}")

    if not changes and not stats["stale"]:
        console.print("[green]Shelf is clean. Nothing to prune.[/green]")


# ──────────────────────────────────────────────
# shelfai version
# ──────────────────────────────────────────────


@app.command("review")
def review_cmd(
    approve: Optional[str] = typer.Option(None, "--approve", help="Staged review file to approve"),
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
):
    """List or approve staged new-context proposals."""
    from shelfai.core.shelf import Shelf

    shelf = Shelf(shelf_path)
    if not shelf.exists():
        console.print(f"[red]No shelf found at {shelf_path}. Run `shelfai init` first.[/red]")
        raise typer.Exit(1)

    stage_dir = shelf.path / "generated" / "review" / "new_context"
    stage_dir.mkdir(parents=True, exist_ok=True)

    if approve:
        stage_file = stage_dir / approve
        if not stage_file.exists():
            console.print(f"[red]Staged file not found: {approve}[/red]")
            raise typer.Exit(1)
        raw = stage_file.read_text(encoding="utf-8")
        proposed_path = ""
        proposed_abstract = ""
        content_summary = ""
        for line in raw.splitlines():
            if line.startswith("proposed_path: "):
                proposed_path = line.split(": ", 1)[1].strip()
            elif line.startswith("proposed_abstract: "):
                proposed_abstract = line.split(": ", 1)[1].strip()
        marker = "## Content Summary\n"
        if marker in raw:
            content_summary = raw.split(marker, 1)[1].strip()

        if not proposed_path or not proposed_abstract:
            console.print("[red]Staged review file is malformed.[/red]")
            raise typer.Exit(1)

        from shelfai.agents.session import _validate_new_context_path, _sanitize_abstract
        config = shelf.load_config()
        _validate_new_context_path(proposed_path, config.session.new_context_allowed_categories or ["memory"])
        target = shelf.path / proposed_path
        if target.exists():
            console.print(f"[red]Target file already exists: {proposed_path}[/red]")
            raise typer.Exit(1)
        today = datetime.now().strftime("%Y-%m-%d")
        title = Path(proposed_path).stem.replace("_", " ").replace("-", " ").title()
        title = title.replace("\n", " ").replace(":", " -").replace("---", "").strip() or "Untitled"
        file_category = Path(proposed_path).parts[0] if len(Path(proposed_path).parts) > 1 else "unknown"
        body = (
            f"---\n"
            f"title: {title}\n"
            f"type: {file_category}\n"
            f"created: {today}\n"
            f"updated: {today}\n"
            f"source: session-review\n"
            f"---\n\n"
            f"# {title}\n\n"
            f"{content_summary}\n"
        )
        shelf.write_file(proposed_path, body)
        shelf.add_index_entry(proposed_path, _sanitize_abstract(proposed_abstract, config.index.abstract_max_length))
        stage_file.unlink()
        console.print(f"[green]Approved staged file into {proposed_path}[/green]")
        return

    staged = sorted(stage_dir.glob("*.md"))
    if not staged:
        console.print("[green]No staged new-context files awaiting review.[/green]")
        return
    console.print("\n[bold]Staged new-context files[/bold]\n")
    for item in staged:
        console.print(f"  - {item.name}")
    console.print("\nApprove one with: [bold]shelfai review --approve <filename>[/bold]")


# ──────────────────────────────────────────────
# shelfai chunk-scan
# ──────────────────────────────────────────────


@app.command("chunk-scan")
def chunk_scan_cmd(
    agents_dir: str = typer.Argument("./agents", help="Path to agents/ directory"),
    min_lines: int = typer.Option(150, "--min-lines", help="Minimum lines to flag unchunked agent"),
    review_threshold: int = typer.Option(80, "--review-threshold", help="Flag existing chunks above this line count"),
):
    """Scan an agents directory for chunking candidates and oversized chunks."""
    from shelfai.core.chunker import scan_agents

    agents_path = Path(agents_dir).resolve()
    if not agents_path.exists():
        console.print(f"[red]Agents directory not found: {agents_path}[/red]")
        raise typer.Exit(1)

    result = scan_agents(agents_path, min_lines=min_lines, review_threshold=review_threshold)

    # Display candidates
    if result.candidates:
        console.print(f"\n[bold]Chunk candidates ({len(result.candidates)}):[/bold]\n")
        table = Table(show_header=True)
        table.add_column("Agent", style="cyan")
        table.add_column("Lines", justify="right")
        table.add_column("File")
        for c in result.candidates:
            table.add_row(c.agent_name, str(c.line_count), str(c.path))
        console.print(table)
    else:
        console.print("\n[green]No unchunked agents above threshold.[/green]")

    # Display reviews
    if result.reviews:
        console.print(f"\n[bold]Chunks flagged for review ({len(result.reviews)}):[/bold]\n")
        table = Table(show_header=True)
        table.add_column("Agent", style="cyan")
        table.add_column("Chunk")
        table.add_column("Lines", justify="right")
        table.add_column("Reason", style="yellow")
        for r in result.reviews:
            table.add_row(r.agent_dir, r.chunk_name, str(r.line_count), r.reason)
        console.print(table)

    if result.ok_count:
        console.print(f"\n[green]{result.ok_count} chunked agent(s) look good.[/green]")

    console.print()


# ──────────────────────────────────────────────
# shelfai chunk
# ──────────────────────────────────────────────


@app.command("chunk")
def chunk_cmd(
    agent_file: str = typer.Argument(..., help="Path to monolithic AGENT.md file"),
    out: Optional[str] = typer.Option(None, "--out", "-o", help="Output directory for chunk files (default: agent's chunks/ dir)"),
    dry_run: bool = typer.Option(False, "--dry-run", help="Preview chunks without writing files"),
    write: bool = typer.Option(False, "--write", help="Write chunk files and router (requires confirmation)"),
):
    """Run the heuristic pre-filter on a monolithic agent file.

    Extracts soul/rules/read-order as always-loaded chunks. Remaining sections
    are left as remaining for the LLM semantic pass to group by deliverable.

    By default, shows a preview. Use --write to actually create chunk files.
    """
    from shelfai.core.chunker import prefilter

    filepath = Path(agent_file).resolve()
    if not filepath.exists():
        console.print(f"[red]File not found: {agent_file}[/red]")
        raise typer.Exit(1)

    result = prefilter(filepath)

    # Display summary
    console.print(f"\n[bold]Pre-filter results for {filepath.name}[/bold]\n")
    console.print(f"  Original:      {result.original_lines} lines")
    console.print(f"  Always-loaded: {result.always_lines} lines")
    console.print(f"  Savings:       ~{result.savings_pct}%\n")

    table = Table(show_header=True)
    table.add_column("Chunk", style="cyan")
    table.add_column("Class")
    table.add_column("Lines", justify="right")
    for name, chunk in result.chunks.items():
        style = "green" if chunk["class"] == "always" else "yellow" if name == "remaining" else ""
        table.add_row(name, chunk["class"], str(chunk["lines"]), style=style)
    console.print(table)

    if "remaining" in result.chunks:
        remaining = result.chunks["remaining"]
        if "sections" in remaining and remaining["sections"]:
            console.print(f"\n[dim]Remaining sections for LLM pass ({len(remaining['sections'])}):[/dim]")
            for s in remaining["sections"]:
                console.print(f"  [dim]- {s['title']} ({s['lines']} lines)[/dim]")

            # Generate the semantic chunking prompt for the user's LLM
            from shelfai.prompts import format_semantic_chunk_prompt
            prompt = format_semantic_chunk_prompt(remaining["sections"])
            console.print("\n[bold]Semantic chunking prompt[/bold] (copy this to your orchestrating LLM):\n")
            console.print(f"[dim]{'─' * 72}[/dim]")
            console.print(prompt)
            console.print(f"[dim]{'─' * 72}[/dim]")

    if dry_run or not write:
        if not write:
            console.print("\n[dim]Preview only. Use --write to create chunk files.[/dim]")
        console.print()
        return

    # Write chunk files
    out_dir = Path(out) if out else filepath.parent / "chunks"
    out_dir.mkdir(parents=True, exist_ok=True)

    written = []
    for name, chunk in result.chunks.items():
        if name == "remaining":
            continue  # Skip remaining — that's for the LLM pass
        chunk_path = out_dir / f"{name}.md"
        chunk_path.write_text(chunk["content"].strip() + "\n", encoding="utf-8")
        written.append(f"{name}.md")

    # Write router
    router_path = filepath.parent / "AGENT.md"
    backup_path = filepath.parent / "AGENT.md.pre-chunk"

    if filepath.name == "AGENT.md" and not backup_path.exists():
        # Back up original
        shutil.copy2(filepath, backup_path)
        console.print(f"  [dim]Backed up original to {backup_path.name}[/dim]")

    router_path.write_text(result.router + "\n", encoding="utf-8")
    written.append("AGENT.md (router)")

    console.print(f"\n[green]Wrote {len(written)} files:[/green]")
    for f in written:
        console.print(f"  [green]✓[/green] {f}")
    console.print("\n[yellow]Note: remaining sections still need the LLM semantic pass.[/yellow]")
    console.print("[dim]Run your semantic chunking pass next to split remaining.md by workflow.[/dim]\n")


# ──────────────────────────────────────────────
# shelfai compact
# ──────────────────────────────────────────────


@app.command("compact")
def compact_cmd(
    shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to shelf directory"),
    agents_dir: Optional[str] = typer.Option(None, "--agents", "-a", help="Path to agents/ directory (scan MEMORY.md files too)"),
    stale_days: int = typer.Option(90, "--stale-days", help="Archive entries older than this many days"),
    scan_only: bool = typer.Option(False, "--scan", help="Only scan and report stats, don't compact"),
    write: bool = typer.Option(False, "--write", help="Apply compaction (writes files, backs up originals)"),
    file: Optional[str] = typer.Option(None, "--file", "-f", help="Compact a specific memory file instead of all"),
):
    """Consolidate memory files — remove duplicates, archive stale entries.

    By default, shows a preview of what would change. Use --write to apply.
    Use --scan to see stats across all memory files without compacting.
    """
    from shelfai.core.compactor import scan_memory, compact_heuristic

    shelf = Path(shelf_path).resolve()

    # Single file mode
    if file:
        filepath = Path(file).resolve()
        if not filepath.exists():
            console.print(f"[red]File not found: {file}[/red]")
            raise typer.Exit(1)

        result = compact_heuristic(filepath, stale_days=stale_days)
        _display_compact_result(result)

        if write and result.savings_pct > 0:
            backup = filepath.with_suffix(".md.pre-compact")
            if not backup.exists():
                shutil.copy2(filepath, backup)
                console.print(f"  [dim]Backed up to {backup.name}[/dim]")
            filepath.write_text(result.content, encoding="utf-8")
            console.print("  [green]Written.[/green]\n")
        elif not write and result.savings_pct > 0:
            console.print("\n[dim]Preview only. Use --write to apply.[/dim]\n")
        return

    # Scan mode
    agents_path = Path(agents_dir).resolve() if agents_dir else None
    scan = scan_memory(shelf, agents_dir=agents_path, stale_days=stale_days)

    if not scan.files:
        console.print("[yellow]No memory files found.[/yellow]")
        raise typer.Exit(0)

    # Display scan results
    console.print("\n[bold]Memory File Scan[/bold]\n")

    table = Table(show_header=True)
    table.add_column("File", style="cyan", max_width=40)
    table.add_column("Entries", justify="right")
    table.add_column("Tokens", justify="right")
    table.add_column("Dupes", justify="right")
    table.add_column("Stale", justify="right")
    table.add_column("Status")

    for f in scan.files:
        status = "[yellow]compact[/yellow]" if f.needs_compaction else "[green]ok[/green]"
        # Show relative path for readability
        try:
            rel = f.path.relative_to(shelf.parent)
        except ValueError:
            rel = f.path.name
        table.add_row(
            str(rel),
            str(f.total_entries),
            f"~{f.estimated_tokens:,}",
            str(f.duplicate_groups) if f.duplicate_groups else "-",
            str(f.stale_entries) if f.stale_entries else "-",
            status,
        )

    console.print(table)
    console.print(f"\n  Total: {scan.total_entries} entries, ~{scan.total_tokens:,} tokens")
    console.print(f"  Files needing compaction: {scan.files_needing_compaction}/{len(scan.files)}")

    if scan_only:
        console.print()
        return

    # Compact files that need it
    needs_work = [f for f in scan.files if f.needs_compaction]
    if not needs_work:
        console.print("\n[green]All memory files are clean. Nothing to compact.[/green]\n")
        return

    console.print("\n[bold]Compaction Preview[/bold]\n")

    results = []
    for stats in needs_work:
        result = compact_heuristic(stats.path, stale_days=stale_days)
        results.append(result)
        _display_compact_result(result)

    total_saved = sum(r.original_tokens - r.compacted_tokens for r in results)

    if write:
        written = 0
        for result in results:
            if result.savings_pct > 0:
                backup = result.path.with_suffix(".md.pre-compact")
                if not backup.exists():
                    shutil.copy2(result.path, backup)
                result.path.write_text(result.content, encoding="utf-8")
                written += 1
        console.print(f"\n[green]Compacted {written} files (~{total_saved:,} tokens saved).[/green]\n")
    else:
        console.print("\n[dim]Preview only. Use --write to apply changes.[/dim]")
        console.print(f"[dim]Would save ~{total_saved:,} tokens across {len(needs_work)} files.[/dim]\n")


def _display_compact_result(result):
    """Pretty-print a single compaction result."""
    try:
        name = result.path.name
    except Exception:
        name = str(result.path)

    console.print(f"  [bold]{name}[/bold]")
    for change in result.changes:
        console.print(f"    {change}")
    if result.savings_pct > 0:
        console.print(
            f"    {result.original_entries} → {result.compacted_entries} entries, "
            f"~{result.savings_pct}% token savings"
        )
    console.print()


@app.command()
def version():
    """Show ShelfAI version."""
    console.print(f"shelfai {__version__}")


# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────


def _display_session_report(report):
    """Pretty-print a session manager report."""

    if report.extraction.is_empty and not report.applied_changes:
        console.print("\n[dim]No notable learnings found in this session.[/dim]\n")
        return

    mode = " [yellow](dry run)[/yellow]" if report.dry_run else ""
    console.print(f"\n[bold]Session Analysis Complete{mode}[/bold]\n")

    # Summary
    ext = report.extraction
    summary_parts = []
    if ext.user_preferences:
        summary_parts.append(f"{len(ext.user_preferences)} user preferences")
    if ext.agent_lessons:
        summary_parts.append(f"{len(ext.agent_lessons)} agent lessons")
    if ext.task_patterns:
        summary_parts.append(f"{len(ext.task_patterns)} task patterns")
    if ext.knowledge_corrections:
        summary_parts.append(f"{len(ext.knowledge_corrections)} corrections flagged")
    if ext.new_context:
        mode = "staged files" if report.applied_changes and any(c.get("type") == "staged_new_file" for c in report.applied_changes) else "new files"
        summary_parts.append(f"{len(ext.new_context)} {mode}")
    if ext.abstract_updates:
        summary_parts.append(f"{len(ext.abstract_updates)} abstract updates")

    if summary_parts:
        console.print(f"  Extracted: {', '.join(summary_parts)}\n")

    # Detail
    icons = {
        "user_preference": "👤",
        "agent_lesson": "🧠",
        "task_pattern": "🔄",
        "knowledge_correction": "⚠️ ",
        "new_file": "📄",
        "abstract_update": "📝",
        "staged_new_file": "🗂️ ",
        "info": "ℹ️ ",
    }

    for change in report.applied_changes:
        ctype = change.get("type", "unknown")
        icon = icons.get(ctype, "•")

        if change.get("type") == "info":
            console.print(f"  {icon} [dim]{change['message']}[/dim]")
        elif "entries" in change:
            for entry in change["entries"]:
                file_label = f"[dim][{change.get('file', '')}][/dim] " if change.get("file") else ""
                console.print(f"  {icon} {file_label}{entry}")
        elif "abstract" in change:
            console.print(f"  {icon} [dim][{change.get('file', '')}][/dim] {change['abstract']}")
        elif "new_abstract" in change:
            console.print(f"  {icon} Updated: [cyan]{change.get('file', '')}[/cyan]")
        elif "content" in change:
            file_label = f"[dim][{change.get('file', '')}][/dim] " if change.get("file") else ""
            console.print(f"  {icon} {file_label}{change['content']}")

    if report.errors:
        console.print(f"\n[red]Errors ({len(report.errors)}):[/red]")
        for error in report.errors:
            console.print(f"  [red]✗[/red] {error['type']}: {error['error']}")

    console.print()
    total = report.extraction.total_items
    verb = "would be applied" if report.dry_run else "applied"
    console.print(f"[bold]{total} changes {verb}.[/bold]\n")


def _get_template_index(template: str) -> str:
    """Get starter index content for a template."""
    if template == "agent":
        return (
            "# ShelfAI Index\n\n"
            "## Skills\n\n"
            "## Knowledge\n\n"
            "## Memory\n"
            "- **memory/user/preferences.md** — User communication preferences, tool choices, and workflow habits.\n"
            "- **memory/agent/lessons.md** — Lessons learned from past sessions. Gotchas, workarounds, correct approaches.\n"
            "- **memory/agent/patterns.md** — Recurring task patterns and user workflow sequences.\n\n"
            "## Resources\n\n"
        )
    elif template == "research":
        return (
            "# ShelfAI Index\n\n"
            "## Skills\n\n"
            "## Knowledge\n\n"
            "## Memory\n"
            "- **memory/user/preferences.md** — User research preferences, citation style, writing tone.\n"
            "- **memory/agent/lessons.md** — Research insights and methodology notes from past sessions.\n"
            "- **memory/agent/patterns.md** — Common research workflows and analysis patterns.\n\n"
            "## Resources\n\n"
        )
    else:  # minimal
        return (
            "# ShelfAI Index\n\n"
            "## Skills\n\n"
            "## Knowledge\n\n"
            "## Memory\n"
            "- **memory/user/preferences.md** — User preferences and habits.\n"
            "- **memory/agent/lessons.md** — Lessons learned from past sessions.\n"
            "- **memory/agent/patterns.md** — Recurring task patterns.\n\n"
            "## Resources\n\n"
        )


# ──────────────────────────────────────────────
# shelfai skill
# ──────────────────────────────────────────────


@app.command("skill")
def skill_cmd(
    show: Optional[str] = typer.Option(None, "--show", help="Print a bundled skill to stdout (e.g. 'claude')"),
    list_all: bool = typer.Option(False, "--list", help="List all bundled skills"),
    install: Optional[str] = typer.Option(None, "--install", help="Copy a bundled skill into a directory"),
    dest: str = typer.Option(".", "--dest", "-d", help="Destination directory for --install"),
):
    """List, show, or install bundled ShelfAI skills."""
    from shelfai.skills import list_skills, get_skill_content, get_skill_path

    if list_all:
        skills = list_skills()
        if not skills:
            console.print("[dim]No bundled skills found.[/dim]")
            return
        console.print("\n[bold]Bundled skills:[/bold]\n")
        for name in skills:
            console.print(f"  - {name}")
        console.print("\nUse [bold]shelfai skill --show <name>[/bold] to view a skill.")
        console.print("Use [bold]shelfai skill --install <name>[/bold] to copy it locally.\n")
        return

    if show:
        try:
            content = get_skill_content(show)
            console.print(content)
        except FileNotFoundError as e:
            console.print(f"[red]{e}[/red]")
            raise typer.Exit(1)
        return

    if install:
        try:
            src = get_skill_path(install)
            dest_path = Path(dest) / install / "SKILL.md"
            dest_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src, dest_path)
            console.print(f"[green]Installed skill '{install}' to {dest_path}[/green]")
        except FileNotFoundError as e:
            console.print(f"[red]{e}[/red]")
            raise typer.Exit(1)
        return

    # No flags — show help
    console.print("Use [bold]shelfai skill --list[/bold] to see available skills.")
    console.print("Use [bold]shelfai skill --show claude[/bold] to view the Claude skill.")
