"""CLI commands for cokodo-agent."""

import atexit
from pathlib import Path
from typing import Optional

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

from cokodo_agent.config import (
    AI_TOOLS,
    BUNDLED_PROTOCOL_VERSION,
    COKODO_HOME_DIR,
    IDE_SPEC_VERSIONS,
    VERSION,
    _migrate_old_cache,
)
from cokodo_agent.fetcher import get_protocol
from cokodo_agent.generator import generate_adapters_for_tools, generate_protocol
from cokodo_agent.parser import HybridParser
from cokodo_agent.prompts import prompt_config
from cokodo_agent.updater import UpdateChecker, print_update_notice

app = typer.Typer(
    name="cokodo",
    help="Cokodo Agent - AI collaboration protocol generator",
    add_completion=False,
)
console = Console()

_update_checker = UpdateChecker()


def _atexit_update_notice() -> None:
    # Wait up to _NETWORK_TIMEOUT seconds so the background PyPI request has
    # time to finish.  0.5 s was too short when a real network call is needed.
    from cokodo_agent.updater import _NETWORK_TIMEOUT
    result = _update_checker.result(timeout=float(_NETWORK_TIMEOUT))
    if result:
        current, latest = result
        print_update_notice(current, latest)


atexit.register(_atexit_update_notice)


def _cleanup_stale_pip_remnants() -> None:
    """Remove ~okodo* / ~-kodo* directories left by interrupted pip upgrades.

    pip renames the old package directory to a ~-prefixed temp name before
    installing the new version.  If the upgrade is interrupted (e.g. because
    an AI tool held a file handle), these directories are never deleted.
    This function removes them silently; a single dim notice is printed to
    stderr so the user knows what happened.
    """
    import shutil
    import site

    removed: list[str] = []
    try:
        dirs = site.getsitepackages()
    except AttributeError:
        dirs = []
    # Also include user site-packages
    try:
        user_sp = site.getusersitepackages()
        if user_sp not in dirs:
            dirs.append(user_sp)
    except AttributeError:
        pass

    for sp in dirs:
        try:
            for entry in Path(sp).iterdir():
                if entry.name.startswith("~") and "kodo" in entry.name.lower():
                    try:
                        if entry.is_dir():
                            shutil.rmtree(entry)
                        else:
                            entry.unlink()
                        removed.append(entry.name)
                    except OSError:
                        pass
        except OSError:
            pass

    if removed:
        try:
            from rich.console import Console
            Console(stderr=True).print(
                f"[dim]Cleaned up {len(removed)} interrupted pip remnant(s): "
                + ", ".join(removed)
                + "[/dim]"
            )
        except Exception:
            pass


def _try_auto_register() -> None:
    """Silently register the current project if .agent/ is present.

    Non-fatal: any exception is swallowed so it never disrupts a command.
    """
    try:
        agent_dir = Path.cwd() / ".agent"
        if agent_dir.is_dir():
            from cokodo_agent.registry import register_project
            register_project(agent_dir)
    except Exception:
        pass


@app.callback()
def _main_callback(ctx: typer.Context) -> None:
    """Start background update check for all commands except serve."""
    # serve requires clean stdout for MCP stdio transport — skip entirely
    if ctx.invoked_subcommand != "serve":
        _update_checker.start()
        # Run one-time cache migration (no-op after first run)
        _migrate_old_cache()
        # Auto-register current project if .agent/ exists
        _try_auto_register()
        # Clean up any ~okodo* directories left by interrupted pip upgrades
        _cleanup_stale_pip_remnants()


def find_agent_dir(path: Optional[Path] = None) -> Path:
    """Find .agent directory from given path or current directory."""
    target = Path(path) if path else Path.cwd()
    target = target.resolve()

    agent_dir = target / ".agent"
    if agent_dir.exists():
        return agent_dir

    raise FileNotFoundError(f".agent directory not found at {target}")


@app.command()
def init(
    path: Optional[Path] = typer.Argument(
        None,
        help="Target directory (default: current directory)",
    ),
    yes: bool = typer.Option(
        False,
        "--yes",
        "-y",
        help="Skip prompts, use defaults",
    ),
    name: Optional[str] = typer.Option(
        None,
        "--name",
        "-n",
        help="Project name",
    ),
    stack: Optional[str] = typer.Option(
        None,
        "--stack",
        "-s",
        help="Tech stack (python/rust/qt/mixed/other)",
    ),
    tools: Optional[str] = typer.Option(
        None,
        "--tools",
        "-t",
        help="IDE adapters to generate: cursor,claude,copilot,gemini,all (comma-separated; used with --yes)",
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Overwrite existing .agent directory",
    ),
    online: bool = typer.Option(
        False,
        "--online",
        help="Fetch latest protocol from GitHub Release (requires network)",
    ),
) -> None:
    """Create .agent protocol in target directory."""

    # Header
    console.print()
    console.print(
        Panel.fit(
            f"[bold blue]Cokodo Agent[/bold blue] v{VERSION}",
            border_style="blue",
        )
    )
    console.print()

    # Resolve target path
    target_path = Path(path) if path else Path.cwd()
    target_path = target_path.resolve()

    # Check existing .agent
    agent_dir = target_path / ".agent"
    if agent_dir.exists() and not force:
        console.print(f"[red]Error:[/red] .agent already exists at {agent_dir}")
        console.print("Use --force to overwrite")
        raise typer.Exit(1)

    # Fetch protocol
    console.print("[bold]Fetching protocol...[/bold]")
    try:
        protocol_path, protocol_version = get_protocol(online=online)
        console.print(f"  [green]OK[/green] Protocol v{protocol_version}")
    except Exception as e:
        console.print(f"  [red]Error:[/red] {e}")
        raise typer.Exit(1)

    console.print()

    # Resolve --tools flag (used in both --yes and interactive mode)
    tools_list: Optional[list[str]] = None
    if tools:
        if tools.strip().lower() == "all":
            tools_list = [k for k in AI_TOOLS if AI_TOOLS[k].get("file")]
        else:
            tools_list = [t.strip().lower() for t in tools.split(",") if t.strip()]
            invalid = [t for t in tools_list if t not in AI_TOOLS]
            if invalid:
                console.print(
                    f"[red]Error:[/red] Unknown tool(s): {', '.join(invalid)}. "
                    f"Choose from: {', '.join(AI_TOOLS.keys())}"
                )
                raise typer.Exit(1)

    # Get configuration
    if yes:
        # When --tools is provided, use it; otherwise protocol-only (no adapter files)
        ai_tools: list[str]
        if tools_list is not None:
            ai_tools = tools_list
        else:
            ai_tools = ["cokodo"]
        config = {
            "project_name": name or target_path.name,
            "description": "",
            "tech_stack": stack or "python",
            "ai_tools": ai_tools,
        }
    else:
        # Interactive prompts
        config = prompt_config(
            default_name=name or target_path.name,
            default_stack=stack,
            default_tools=tools_list,
        )

    console.print()

    # Generate
    console.print("[bold]Generating .agent/[/bold]")
    try:
        generated = generate_protocol(
            source_path=protocol_path,
            target_path=target_path,
            config=config,
            force=force,
        )
        for item in generated:
            console.print(f"  [green]OK[/green] {item}")
    except Exception as e:
        console.print(f"  [red]Error:[/red] {e}")
        raise typer.Exit(1)

    console.print()

    # Success message
    next_steps = (
        f"[green]Success![/green] Initialized [bold]{target_path}[/bold]\n\n"
        "[bold]Next steps:[/bold]\n"
        "  1. Review [cyan].agent/project/context.md[/cyan]\n"
        "  2. Customize [cyan].agent/project/tech-stack.md[/cyan]\n"
        "  3. Start coding with AI assistance!"
    )
    # Suggest adapter generation if none were created
    selected_tools = [t for t in config.get("ai_tools", []) if AI_TOOLS.get(t, {}).get("file")]
    if not selected_tools:
        next_steps += (
            "\n\n[dim]Tip: run [cyan]co adapt cursor[/cyan] (or claude/copilot/gemini/all) "
            "to generate IDE adapter files.[/dim]"
        )
    console.print(
        Panel(
            next_steps,
            title="Done",
            border_style="green",
        )
    )


@app.command()
def lint(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    rule: Optional[str] = typer.Option(
        None,
        "--rule",
        "-r",
        help="Check specific rule only",
    ),
    format: str = typer.Option(
        "text",
        "--format",
        "-f",
        help="Output format (text/json/github)",
    ),
) -> None:
    """Check protocol compliance."""
    import json as json_module

    from cokodo_agent.linter import ProtocolLinter

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    linter = ProtocolLinter(agent_dir)

    if rule:
        results = linter.lint_rule(rule)
    else:
        results = linter.lint_all()

    errors = [r for r in results if not r.passed]

    if format == "json":
        print(json_module.dumps([r._asdict() for r in results], indent=2, ensure_ascii=False))

    elif format == "github":
        for r in results:
            if not r.passed:
                file_part = f"file={r.file}" if r.file else ""
                line_part = f",line={r.line}" if r.line else ""
                print(f"::error {file_part}{line_part}::[{r.rule}] {r.message}")
        if not errors:
            print("::notice ::All protocol checks passed")

    else:  # text format
        console.print()
        console.print(
            Panel.fit(
                "[bold]Protocol Compliance Check[/bold]\n"
                f"Based on: agent-protocol-rules.md v{BUNDLED_PROTOCOL_VERSION}",
                border_style="blue",
            )
        )
        console.print()

        # Group by rule
        rules = sorted(set(r.rule for r in results))
        for rule_name in rules:
            rule_results = [r for r in results if r.rule == rule_name]
            passed = sum(1 for r in rule_results if r.passed)
            total = len(rule_results)

            if passed == total:
                console.print(f"[green][OK][/green] {rule_name}: {passed}/{total} passed")
            else:
                console.print(f"[red][FAIL][/red] {rule_name}: {passed}/{total} passed")

                # Show errors
                for r in rule_results:
                    if not r.passed:
                        loc = f"  {r.file}" if r.file else ""
                        if r.line:
                            loc += f":{r.line}"
                        console.print(f"    [red]x[/red]{loc}: {r.message}")

        console.print()
        total_passed = len(results) - len(errors)
        console.print(f"Total: {total_passed}/{len(results)} passed")

        if errors:
            console.print(f"\n[red][FAIL][/red] {len(errors)} error(s) found")
            raise typer.Exit(1)
        else:
            console.print("\n[green][OK][/green] All checks passed")


@app.command("update-checksums")
def update_checksums(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
) -> None:
    """Update checksums in manifest.json (maintainer only)."""
    from cokodo_agent.linter import update_checksums as do_update

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    try:
        checksums = do_update(agent_dir)
        console.print(f"[green]OK[/green] Updated checksums for {len(checksums)} locked files")
        console.print(f"    Written to {agent_dir / 'manifest.json'}")
    except Exception as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)


@app.command()
def diff(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    online: bool = typer.Option(
        False,
        "--online",
        help="Fetch latest protocol from GitHub Release (requires network)",
    ),
) -> None:
    """Compare local .agent with latest protocol."""
    from cokodo_agent.sync import diff_protocol

    offline = not online

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    console.print("[bold]Comparing with latest protocol...[/bold]")
    console.print()

    try:
        results, local_version, remote_version = diff_protocol(agent_dir, offline=offline)
    except Exception as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    console.print(f"Local version:  [cyan]{local_version}[/cyan]")
    console.print(f"Remote version: [cyan]{remote_version}[/cyan]")
    console.print()

    # Count changes
    added = [r for r in results if r.status == "added"]
    removed = [r for r in results if r.status == "removed"]
    modified = [r for r in results if r.status == "modified"]
    unchanged = [r for r in results if r.status == "unchanged"]

    if not added and not removed and not modified:
        console.print("[green]No changes detected. Protocol is up to date.[/green]")
        return

    # Show summary
    table = Table(title="Changes")
    table.add_column("Status", style="bold")
    table.add_column("Count")

    if added:
        table.add_row("[green]Added[/green]", str(len(added)))
    if removed:
        table.add_row("[red]Removed[/red]", str(len(removed)))
    if modified:
        table.add_row("[yellow]Modified[/yellow]", str(len(modified)))
    table.add_row("Unchanged", str(len(unchanged)))

    console.print(table)
    console.print()

    # Show details
    if added:
        console.print("[green]Added files:[/green]")
        for r in added:
            console.print(f"  + {r.path}")
        console.print()

    if removed:
        console.print("[red]Removed files:[/red]")
        for r in removed:
            console.print(f"  - {r.path}")
        console.print()

    if modified:
        console.print("[yellow]Modified files:[/yellow]")
        for r in modified:
            console.print(f"  ~ {r.path}")
        console.print()

    console.print("Run [cyan]co sync[/cyan] to update your protocol.")


@app.command()
def sync(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    online: bool = typer.Option(
        False,
        "--online",
        help="Fetch latest protocol from GitHub Release (requires network)",
    ),
    dry_run: bool = typer.Option(
        False,
        "--dry-run",
        help="Show what would be updated without making changes",
    ),
    yes: bool = typer.Option(
        False,
        "--yes",
        "-y",
        help="Skip confirmation prompt",
    ),
) -> None:
    """Sync local .agent with latest protocol."""
    from cokodo_agent.sync import diff_protocol, sync_protocol

    offline = not online

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    # First show diff
    console.print("[bold]Checking for updates...[/bold]")
    console.print()

    try:
        diff_results, local_version, remote_version = diff_protocol(agent_dir, offline=offline)
    except Exception as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    console.print(f"Local version:  [cyan]{local_version}[/cyan]")
    console.print(f"Remote version: [cyan]{remote_version}[/cyan]")
    console.print()

    # Count changes (excluding project/ files)
    changes = [
        r for r in diff_results if r.status != "unchanged" and not r.path.startswith("project/")
    ]
    version_mismatch = local_version != remote_version

    if not changes and not version_mismatch:
        console.print("[green]Protocol is up to date. No changes needed.[/green]")
        return

    if not changes and version_mismatch:
        console.print(
            f"[yellow]Files are up to date, but local version ({local_version}) "
            f"differs from remote ({remote_version}). Updating manifest...[/yellow]"
        )
    else:
        console.print(f"[yellow]{len(changes)} file(s) will be updated[/yellow]")
    console.print()

    # Confirm
    if not yes and not dry_run:
        confirm = typer.confirm("Proceed with sync?")
        if not confirm:
            console.print("Aborted.")
            raise typer.Exit(0)

    # Sync
    if dry_run:
        console.print("[bold]Dry run - no changes will be made[/bold]")
        console.print()

    try:
        result, _, _ = sync_protocol(agent_dir, offline=offline, dry_run=dry_run)
    except Exception as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    # Show results
    if result.updated:
        console.print("[green]Updated:[/green]")
        for f in result.updated:
            console.print(f"  {f}")
        console.print()

    if result.skipped:
        console.print("[yellow]Skipped:[/yellow]")
        for f in result.skipped:
            console.print(f"  {f}")
        console.print()

    if result.errors:
        console.print("[red]Errors:[/red]")
        for err in result.errors:
            console.print(f"  {err}")
        raise typer.Exit(1)

    if not dry_run:
        console.print(f"[green]OK[/green] Synced to v{remote_version}")


@app.command()
def adapt(
    tool: str = typer.Argument(
        ...,
        help="IDE to generate: cursor | claude | copilot | gemini | all",
    ),
    path: Optional[Path] = typer.Argument(
        None,
        help="Project path (default: current directory)",
    ),
) -> None:
    """Generate IDE entry file(s) from existing .agent protocol."""
    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    target_path = agent_dir.parent
    tool_lower = tool.strip().lower()

    # Support legacy alias
    if tool_lower == "antigravity":
        tool_lower = "gemini"

    if tool_lower == "all":
        tool_keys = [k for k in AI_TOOLS if AI_TOOLS[k].get("file")]
    elif tool_lower in AI_TOOLS:
        tool_keys = [tool_lower]
    else:
        console.print(
            f"[red]Error:[/red] Unknown tool '{tool}'. "
            f"Choose: cursor, claude, copilot, gemini, all"
        )
        raise typer.Exit(1)

    generate_adapters_for_tools(target_path, agent_dir, tool_keys)

    names = [AI_TOOLS[k]["name"] for k in tool_keys]
    console.print(f"[green]OK[/green] Generated: {', '.join(names)}")
    for k in tool_keys:
        info = AI_TOOLS[k]
        file_path = info.get("file")
        if file_path:
            # For directory-mode adapters, show the directory
            out = target_path / file_path
            console.print(f"  -> {out}")


@app.command()
def detect(
    path: Optional[Path] = typer.Argument(
        None,
        help="Project path (default: current directory)",
    ),
) -> None:
    """Detect IDE instruction files (Cursor, Claude, Copilot, Gemini) in the project."""
    target = Path(path) if path else Path.cwd()
    target = target.resolve()
    hybrid = HybridParser()
    detected = hybrid.detect_all(target)
    if not detected:
        console.print("[yellow]No IDE instruction files detected.[/yellow]")
        return
    console.print("[bold]Detected IDE instruction files:[/bold]")
    for tool_name, files in sorted(detected.items()):
        console.print(f"  [cyan]{tool_name}[/cyan]:")
        for f in files:
            console.print(f"    {f.path} ({f.format_version})")


@app.command("import")
def import_rules(
    path: Optional[Path] = typer.Argument(
        None,
        help="Project path (default: current directory)",
    ),
    source: Optional[str] = typer.Option(
        None,
        "--source",
        "-s",
        help="Import from: cursor | claude | copilot | gemini | auto (default: auto = all detected)",
    ),
    dry_run: bool = typer.Option(
        False,
        "--dry-run",
        help="Show what would be imported without writing",
    ),
) -> None:
    """Import rules from existing IDE instruction files into .agent protocol."""
    target = Path(path) if path else Path.cwd()
    target = target.resolve()
    hybrid = HybridParser()
    detected = hybrid.detect_all(target)
    if not detected:
        console.print("[yellow]No IDE instruction files detected. Run from a project that has CLAUDE.md, AGENTS.md, GEMINI.md, or .cursor/rules/.[/yellow]")
        raise typer.Exit(0)

    agent_dir = target / ".agent"
    if not agent_dir.exists():
        console.print("[red]Error:[/red] No .agent directory. Run [cyan]co init[/cyan] first.")
        raise typer.Exit(1)

    tools_to_parse = list(detected.keys()) if source in (None, "auto") else [source]
    if source and source != "auto" and source not in detected:
        console.print(f"[red]Error:[/red] No files detected for '{source}'.")
        raise typer.Exit(1)

    parsed_list: list = []
    for tool in tools_to_parse:
        if tool in detected:
            parsed_list.extend(hybrid.parse_tool(target, tool))

    if not parsed_list:
        console.print("[yellow]Nothing to import.[/yellow]")
        return

    project_name: str | None = None
    all_rules: list[str] = []
    all_refs: set[str] = set()
    for p in parsed_list:
        if p.project_name:
            project_name = project_name or p.project_name
        all_rules.extend(p.rules)
        all_refs.update(p.referenced_files)

    console.print()
    console.print(Panel.fit("[bold]Import summary[/bold]", border_style="blue"))
    console.print(f"  Project name: {project_name or '(not found)'}")
    console.print(f"  Rules extracted: {len(all_rules)}")
    console.print(f"  Referenced .agent files: {len(all_refs)}")
    for r in sorted(all_refs)[:10]:
        console.print(f"    - {r}")
    if len(all_refs) > 10:
        console.print(f"    ... and {len(all_refs) - 10} more")
    console.print()

    if dry_run:
        console.print("[yellow]Dry run. No files written. Remove --dry-run to apply.[/yellow]")
        return

    written: list[str] = []
    if project_name and (agent_dir / "project" / "context.md").exists():
        context_path = agent_dir / "project" / "context.md"
        content = context_path.read_text(encoding="utf-8")
        if "## Project Name" in content:
            lines = content.splitlines()
            out: list[str] = []
            i = 0
            while i < len(lines):
                if "## Project Name" in lines[i]:
                    out.append(lines[i])
                    i += 1
                    if i < len(lines) and lines[i].strip() and not lines[i].startswith("#"):
                        i += 1  # skip old value
                    out.append(project_name)
                    i += 1
                    continue
                out.append(lines[i])
                i += 1
            new_content = "\n".join(out)
            if new_content != content:
                context_path.write_text(new_content, encoding="utf-8")
                written.append("project/context.md (project name)")
    if all_rules and (agent_dir / "project" / "conventions.md").exists():
        conv_path = agent_dir / "project" / "conventions.md"
        content = conv_path.read_text(encoding="utf-8")
        if "## Imported Rules" not in content:
            content = content.rstrip() + "\n\n## Imported Rules\n\n"
        for r in all_rules[:20]:
            if r.strip() and f"- {r}" not in content:
                content += f"- {r}\n"
        conv_path.write_text(content, encoding="utf-8")
        written.append("project/conventions.md (imported rules)")
    elif all_rules:
        conv_path = agent_dir / "project" / "conventions.md"
        conv_path.parent.mkdir(parents=True, exist_ok=True)
        block = "## Imported Rules\n\n" + "\n".join(f"- {r}" for r in all_rules[:20])
        conv_path.write_text("# Project Conventions\n\n" + block + "\n", encoding="utf-8")
        written.append("project/conventions.md (created with imported rules)")

    if written:
        console.print(f"[green]OK[/green] Updated: {', '.join(written)}")
    else:
        console.print("[yellow]No .agent/project files updated (name already set, or no conventions.md).[/yellow]")


@app.command()
def context(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    stack: Optional[str] = typer.Option(
        None,
        "--stack",
        "-s",
        help="Tech stack (python/rust/qt/mixed)",
    ),
    task: Optional[str] = typer.Option(
        None,
        "--task",
        "-t",
        help="Task type (coding/testing/review/documentation/bug_fix/feature_development)",
    ),
    output: str = typer.Option(
        "list",
        "--output",
        "-o",
        help="Output format (list/paths/content)",
    ),
) -> None:
    """Get context files based on stack and task type."""
    from cokodo_agent.context import get_context_files

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    files = get_context_files(agent_dir, stack=stack, task=task)

    if not files:
        console.print("[yellow]No context files found for the given criteria.[/yellow]")
        return

    if output == "paths":
        # Output absolute paths, one per line (for scripting)
        for f in files:
            print(agent_dir / f)

    elif output == "content":
        # Output file contents (for piping to AI)
        for f in files:
            file_path = agent_dir / f
            if file_path.exists():
                console.print(f"[bold]# {f}[/bold]")
                console.print(file_path.read_text(encoding="utf-8"))
                console.print()

    else:  # list
        console.print(
            f"[bold]Context files for stack={stack or 'all'}, task={task or 'all'}:[/bold]"
        )
        console.print()
        for f in files:
            file_path = agent_dir / f
            exists = "[green]OK[/green]" if file_path.exists() else "[red]MISSING[/red]"
            console.print(f"  {exists} {f}")


@app.command()
def journal(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    title: Optional[str] = typer.Option(
        None,
        "--title",
        "-t",
        help="Session title (e.g., 'Feature X implementation')",
    ),
    completed: Optional[str] = typer.Option(
        None,
        "--completed",
        "-c",
        help="Completed items (comma-separated)",
    ),
    debt: Optional[str] = typer.Option(
        None,
        "--debt",
        "-d",
        help="Technical debt items (comma-separated)",
    ),
    decisions: Optional[str] = typer.Option(
        None,
        "--decisions",
        help="Key decisions made (comma-separated)",
    ),
    interactive: bool = typer.Option(
        False,
        "--interactive",
        "-i",
        help="Interactive mode with prompts",
    ),
) -> None:
    """Record a session entry to session-journal.md."""
    from datetime import datetime

    import questionary

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    journal_path = agent_dir / "project" / "session-journal.md"
    if not journal_path.exists():
        console.print(f"[red]Error:[/red] session-journal.md not found at {journal_path}")
        raise typer.Exit(1)

    # Interactive mode
    if interactive or (not title and not completed):
        console.print()
        console.print(
            Panel.fit(
                "[bold blue]Session Journal Entry[/bold blue]",
                border_style="blue",
            )
        )
        console.print()

        title = questionary.text(
            "Session title:",
            default=title or "",
        ).ask()

        if not title:
            console.print("[yellow]Aborted.[/yellow]")
            raise typer.Exit(0)

        completed_input = questionary.text(
            "Completed items (comma-separated):",
            default=completed or "",
        ).ask()

        debt_input = questionary.text(
            "Technical debt (comma-separated, or leave empty):",
            default=debt or "",
        ).ask()

        decisions_input = questionary.text(
            "Key decisions (comma-separated, or leave empty):",
            default=decisions or "",
        ).ask()

        completed = completed_input
        debt = debt_input
        decisions = decisions_input

    # Build entry
    today = datetime.now().strftime("%Y-%m-%d")

    entry_lines = [
        f"\n## {today} Session: {title}",
        "",
        "### Completed",
    ]

    if completed:
        for item in completed.split(","):
            item = item.strip()
            if item:
                entry_lines.append(f"- {item}")
    else:
        entry_lines.append("- (no items recorded)")

    entry_lines.append("")
    entry_lines.append("### Technical Debt")

    if debt:
        for item in debt.split(","):
            item = item.strip()
            if item:
                entry_lines.append(f"- {item}")
    else:
        entry_lines.append("- None")

    entry_lines.append("")
    entry_lines.append("### Decisions")

    if decisions:
        for item in decisions.split(","):
            item = item.strip()
            if item:
                entry_lines.append(f"- {item}")
    else:
        entry_lines.append("- (no decisions recorded)")

    entry_lines.append("")
    entry_lines.append("---")
    entry_lines.append("")

    entry_text = "\n".join(entry_lines)

    # Append to journal
    try:
        content = journal_path.read_text(encoding="utf-8")

        # Find the append marker
        marker = "*Append new sessions below this line.*"
        if marker in content:
            content = content.replace(marker, marker + entry_text)
        else:
            # Just append at the end
            content = content.rstrip() + "\n" + entry_text

        journal_path.write_text(content, encoding="utf-8")

        console.print()
        console.print(f"[green]OK[/green] Added session entry to {journal_path}")
        console.print()
        console.print("[bold]Entry preview:[/bold]")
        console.print(entry_text)

    except Exception as e:
        console.print(f"[red]Error:[/red] Failed to write journal: {e}")
        raise typer.Exit(1)


@app.command()
def status(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    raw: bool = typer.Option(
        False,
        "--raw",
        help="Show raw markdown content",
    ),
    json_output: bool = typer.Option(
        False,
        "--json",
        help="Output as JSON (for scripting)",
    ),
    init_status: bool = typer.Option(
        False,
        "--init",
        help="Create status.md from template if missing",
    ),
) -> None:
    """Show or initialize project development status."""
    import json as json_module
    import re

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    status_file = agent_dir / "project" / "status.md"

    if init_status:
        if status_file.exists():
            console.print("[yellow]status.md already exists.[/yellow]")
            raise typer.Exit(0)
        from datetime import datetime

        today = datetime.now().strftime("%Y-%m-%d")
        template = (
            "# Project Status\n\n"
            f"> Last updated: {today} | Updated by: co status --init\n\n"
            "---\n\n"
            "## Current Phase\n\n"
            "[Describe the current development phase.]\n\n"
            "## Active Goals\n\n"
            "| # | Goal | Priority | Status |\n"
            "|---|------|----------|--------|\n"
            "| 1 | [Goal description] | High | Pending |\n\n"
            "## Task Board\n\n"
            "### In Progress\n"
            "- [ ] [Task description]\n\n"
            "### Pending\n"
            "- [ ] Define development goals\n\n"
            "### Recently Completed\n"
            "- (none yet)\n\n"
            "## Blockers\n\n"
            "None.\n\n"
            "## Session Context\n\n"
            f"- status.md initialized on {today}.\n\n"
            "## Roadmap Overview\n\n"
            "| Phase | Focus | Status |\n"
            "|-------|-------|--------|\n"
            "| [Phase] | [Description] | Current |\n\n"
            "## Quick Reference\n\n"
            "| Item | Value |\n"
            "|------|-------|\n"
            "| Status | Initialized |\n"
        )
        status_file.parent.mkdir(parents=True, exist_ok=True)
        status_file.write_text(template, encoding="utf-8")
        console.print(f"[green]OK[/green] Created {status_file}")
        return

    if not status_file.exists():
        console.print("[yellow]No status.md found.[/yellow]")
        console.print("Run [cyan]co status --init[/cyan] to create one.")
        raise typer.Exit(1)

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

    if raw:
        print(content)
        return

    if json_output:
        sections: dict[str, str] = {}
        current_section = ""
        current_lines: list[str] = []
        for line in content.splitlines():
            if line.startswith("## "):
                if current_section:
                    sections[current_section] = "\n".join(current_lines).strip()
                current_section = line[3:].strip()
                current_lines = []
            else:
                current_lines.append(line)
        if current_section:
            sections[current_section] = "\n".join(current_lines).strip()
        print(json_module.dumps(sections, indent=2, ensure_ascii=False))
        return

    # Formatted display
    console.print()
    console.print(
        Panel.fit("[bold blue]Project Status[/bold blue]", border_style="blue")
    )
    console.print()

    current_section = ""
    for line in content.splitlines():
        if line.startswith("# Project Status"):
            continue
        if line.startswith("> Last updated:"):
            console.print(f"  [dim]{line.lstrip('> ')}[/dim]")
            console.print()
            continue
        if line.strip() == "---":
            continue

        if line.startswith("## "):
            section_name = line[3:].strip()
            current_section = section_name
            console.print(f"[bold cyan]{section_name}[/bold cyan]")
            continue
        if line.startswith("### "):
            console.print(f"  [bold]{line[4:].strip()}[/bold]")
            continue

        stripped = line.strip()
        if not stripped:
            continue

        if stripped.startswith("| #") or stripped.startswith("|---"):
            continue
        if stripped.startswith("|") and not stripped.startswith("| Phase") and not stripped.startswith("| Item"):
            cols = [c.strip() for c in stripped.split("|")[1:-1]]
            if len(cols) >= 3:
                console.print(f"  {cols[0]}. {cols[1]} [{cols[-1]}]")
            elif len(cols) >= 2:
                console.print(f"  {cols[0]}: {cols[1]}")
            continue
        if stripped.startswith("| Phase") or stripped.startswith("| Item"):
            continue

        if stripped.startswith("- [x]"):
            console.print(f"  [green][x][/green] {stripped[6:]}")
        elif stripped.startswith("- [ ]"):
            console.print(f"  [yellow][ ][/yellow] {stripped[6:]}")
        elif stripped.startswith("- "):
            console.print(f"  {stripped}")
        else:
            console.print(f"  {stripped}")

    console.print()
    console.print(f"[dim]Source: {status_file}[/dim]")


@app.command()
def scaffold(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Overwrite existing files (re-create from template)",
    ),
    yes: bool = typer.Option(
        False,
        "--yes",
        "-y",
        help="Skip confirmation prompt",
    ),
) -> None:
    """Audit and scaffold missing project/ files from templates.

    Shows which required files are missing or contain stub content,
    then creates missing files from templates. Use --force to re-create
    all files including existing ones.
    """
    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    project_dir = agent_dir / "project"
    project_dir.mkdir(parents=True, exist_ok=True)

    # Aligned with co lint: use same list as ProtocolLinter.REQUIRED_PROJECT_FILES
    from cokodo_agent.linter import ProtocolLinter
    scaffold_files = [
        (f, ProtocolLinter.PROJECT_FILE_DESCRIPTIONS.get(f, f))
        for f in ProtocolLinter.REQUIRED_PROJECT_FILES
    ]

    # SOP files in project/sop/
    sop_dir = project_dir / "sop"
    sop_dir.mkdir(parents=True, exist_ok=True)
    scaffold_sop_files = list(ProtocolLinter.REQUIRED_SOP_FILES.items())  # [(name, desc)]

    # Template placeholder patterns — file exists but hasn't been customized
    _STUB_MARKERS = ("{{", "[DATE]", "[Describe", "[Your ", "[Project ", "{{PROJECT_NAME}}")

    def _is_stub(file_path: Path) -> bool:
        """Return True if file contains unreplaced template placeholders."""
        try:
            text = file_path.read_text(encoding="utf-8")
            return any(marker in text for marker in _STUB_MARKERS)
        except OSError:
            return False

    def _find_template(filename: str, subdir: str = "") -> Optional[Path]:
        """Find template: local .agent/templates/project/ first, then bundled.

        Args:
            filename: Template filename (e.g. "context.md").
            subdir: Optional subdirectory under templates/project/ (e.g. "sop").
        """
        base = f"{subdir}/{filename}" if subdir else filename
        local = agent_dir / "templates" / "project" / base
        if local.exists():
            return local
        # Bundled fallback
        from cokodo_agent.fetcher import get_protocol
        try:
            bundled_path, _ = get_protocol(online=False)
            bundled = bundled_path / "templates" / "project" / base
            if bundled.exists():
                return bundled
        except Exception:
            pass
        return None

    # Audit all files
    rows: list[tuple[str, str, str, bool]] = []  # (name, status_label, note, will_create)
    for filename, desc in scaffold_files:
        target = project_dir / filename
        if target.exists():
            if force:
                rows.append((filename, "[yellow]EXISTS[/yellow]", "Will overwrite (--force)", True))
            elif _is_stub(target):
                rows.append((filename, "[yellow]STUB[/yellow]", "Contains placeholders, skipping", False))
            else:
                rows.append((filename, "[green]OK[/green]", "", False))
        else:
            rows.append((filename, "[red]MISSING[/red]", "Will create", True))

    # Audit SOP files
    sop_rows: list[tuple[str, str, str, bool]] = []  # (name, status_label, note, will_create)
    for sop_name, sop_desc in scaffold_sop_files:
        target = sop_dir / sop_name
        if target.exists():
            if force:
                sop_rows.append((sop_name, "[yellow]EXISTS[/yellow]", "Will overwrite (--force)", True))
            elif _is_stub(target):
                sop_rows.append((sop_name, "[yellow]STUB[/yellow]", "Contains placeholders, skipping", False))
            else:
                sop_rows.append((sop_name, "[green]OK[/green]", "", False))
        else:
            sop_rows.append((sop_name, "[red]MISSING[/red]", "Will create", True))

    # Display audit table
    console.print()
    console.print(Panel.fit(
        f"[bold]Project File Audit[/bold]\n{agent_dir / 'project'}",
        border_style="blue",
    ))
    console.print()

    table = Table(show_header=True, header_style="bold")
    table.add_column("File", style="cyan", width=28)
    table.add_column("Status", width=10)
    table.add_column("Description", style="dim", width=42)
    table.add_column("Note", width=30)

    for filename, status_label, note, _ in rows:
        desc_text = next(d for f, d in scaffold_files if f == filename)
        table.add_row(filename, status_label, desc_text, note)

    if sop_rows:
        table.add_section()
        for sop_name, status_label, note, _ in sop_rows:
            desc_text = ProtocolLinter.REQUIRED_SOP_FILES.get(sop_name, sop_name)
            table.add_row(f"sop/{sop_name}", status_label, desc_text, note)

    console.print(table)
    console.print()

    to_create = [(filename, note) for filename, _, note, will in rows if will]
    sop_to_create = [(sop_name, note) for sop_name, _, note, will in sop_rows if will]

    if not to_create and not sop_to_create:
        console.print("[green]All required project files are present.[/green]")
        if any("STUB" in status for _, status, _, _ in rows) or any("STUB" in status for _, status, _, _ in sop_rows):
            console.print()
            console.print(
                "[dim]Stub files exist but contain placeholder content. "
                "Edit them manually or use --force to reset from template.[/dim]"
            )
        return

    total = len(to_create) + len(sop_to_create)
    console.print(f"[bold]{total}[/bold] file(s) to create/overwrite.")
    console.print()

    if not yes:
        confirmed = typer.confirm("Proceed?", default=True)
        if not confirmed:
            console.print("[yellow]Cancelled.[/yellow]")
            raise typer.Exit(0)

    # Create project/ files
    created: list[str] = []
    skipped: list[str] = []
    errors: list[str] = []

    for filename, _ in to_create:
        target = project_dir / filename
        template_path = _find_template(filename)

        if template_path is None:
            errors.append(f"{filename}: template not found")
            continue

        try:
            content = template_path.read_text(encoding="utf-8")
            target.write_text(content, encoding="utf-8")
            created.append(f"project/{filename}")
        except OSError as e:
            errors.append(f"{filename}: {e}")

    # Create project/sop/ files
    for sop_name, _ in sop_to_create:
        target = sop_dir / sop_name
        template_path = _find_template(sop_name, subdir="sop")

        if template_path is None:
            errors.append(f"sop/{sop_name}: template not found")
            continue

        try:
            content = template_path.read_text(encoding="utf-8")
            target.write_text(content, encoding="utf-8")
            created.append(f"project/sop/{sop_name}")
        except OSError as e:
            errors.append(f"sop/{sop_name}: {e}")

    console.print()
    if created:
        console.print(f"[green]Created {len(created)} file(s):[/green]")
        for f in created:
            console.print(f"  + {f}")
    if errors:
        console.print(f"[red]Errors ({len(errors)}):[/red]")
        for e in errors:
            console.print(f"  x {e}")

    console.print()
    console.print("[dim]Next steps:[/dim]")
    console.print("  1. Edit the created files with your project-specific information")
    console.print("  2. Run [bold]co lint[/bold] to verify compliance")
    console.print("  3. Run [bold]co status[/bold] to view current project state")


@app.command()
def serve(
    path: Optional[Path] = typer.Argument(
        None,
        help="Path to project (default: current directory)",
    ),
    transport: str = typer.Option(
        "stdio",
        "--transport",
        "-t",
        help="Transport mode (stdio/streamable-http)",
    ),
    workspace: bool = typer.Option(
        False,
        "--workspace",
        "-w",
        help="Workspace mode: discover and serve all .agent/ projects under path",
    ),
    global_mode: bool = typer.Option(
        False,
        "--global",
        "-g",
        help="Global mode: serve all projects from the local registry (~/.cokodo/)",
    ),
) -> None:
    """Start MCP server for IDE integration.

    Exposes .agent/ protocol context via Model Context Protocol.
    IDEs like Cursor and Claude Code connect automatically via stdio.

    Use --workspace to discover and serve multiple projects at once.
    Use --global to serve all cokodo projects from the local registry.

    Requires: pip install cokodo-agent[mcp]
    """
    try:
        import mcp  # noqa: F401
    except ImportError:
        err = Console(stderr=True)
        err.print("[red]Error:[/red] MCP support not installed.")
        err.print()
        err.print("Install with:")
        err.print("  [bold]pip install cokodo-agent[mcp][/bold]")
        raise typer.Exit(1)

    # Silence mcp SDK INFO logs on stderr — Cursor treats all stderr as errors,
    # causing false-alarm noise in the MCP panel. WARNING+ still surface real issues.
    import logging
    logging.getLogger("mcp").setLevel(logging.WARNING)

    if global_mode:
        from cokodo_agent.mcp_server import create_global_server

        server = create_global_server()
        server.run(transport=transport)
        return

    if workspace:
        workspace_root = Path(path).resolve() if path else Path.cwd().resolve()
        from cokodo_agent.mcp_server import create_workspace_server

        server = create_workspace_server(workspace_root)
        server.run(transport=transport)
        return

    if path:
        agent_dir: Optional[Path] = Path(path).resolve() / ".agent"
        if not agent_dir.is_dir():
            err = Console(stderr=True)
            err.print(f"[red]Error:[/red] No .agent/ directory found in {path}")
            raise typer.Exit(1)
    else:
        agent_dir = None

    from cokodo_agent.mcp_server import create_server

    server = create_server(agent_dir=agent_dir)
    server.run(transport=transport)


@app.command()
def version() -> None:
    """Show version information."""
    console.print(f"cokodo-agent v{VERSION}")
    console.print()
    console.print("Protocol versions:")
    console.print(f"  Built-in: v{BUNDLED_PROTOCOL_VERSION}")
    console.print()
    console.print("IDE spec versions (parser/generator target):")
    for tool, info in sorted(IDE_SPEC_VERSIONS.items()):
        console.print(f"  {tool}: {info['spec_version']} (validated {info['spec_date']})")


# ---------------------------------------------------------------------------
# Cross-project: co ref
# ---------------------------------------------------------------------------


@app.command()
def ref(
    action: str = typer.Argument(
        ...,
        help="Action: list, add, remove, check",
    ),
    source: Optional[str] = typer.Argument(
        None,
        help="Source path (for add) or reference name (for remove)",
    ),
    path: Optional[Path] = typer.Option(
        None,
        "--path",
        "-p",
        help="Path to project (default: current directory)",
    ),
    name: Optional[str] = typer.Option(
        None,
        "--name",
        "-n",
        help="Reference name (for add; defaults to directory name)",
    ),
    description: Optional[str] = typer.Option(
        None,
        "--description",
        "-d",
        help="Reference description",
    ),
    scope: Optional[str] = typer.Option(
        None,
        "--scope",
        "-s",
        help="Comma-separated scope patterns (for add)",
    ),
    use_when: Optional[str] = typer.Option(
        None,
        "--use-when",
        help="Loading strategy: always / session_start / on_demand",
    ),
) -> None:
    """Manage references to other projects and documents."""
    from cokodo_agent.relations import (
        add_reference_to_manifest,
        check_references,
        detect_source_type,
        load_references,
        remove_reference_from_manifest,
    )
    from cokodo_agent.remote import is_remote_source

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    project_root = agent_dir.parent

    if action == "list":
        refs = load_references(agent_dir)
        if not refs:
            console.print("No references declared.")
            console.print()
            console.print(
                "Add one with: [cyan]co ref add <path> --name <name>[/cyan]"
            )
            return

        statuses = check_references(agent_dir, project_root)
        status_map = {s.name: s for s in statuses}

        table = Table(title="Declared References")
        table.add_column("Name", style="bold")
        table.add_column("Source")
        table.add_column("Type")
        table.add_column("When")
        table.add_column("Status")

        for r in refs:
            st = status_map.get(r.name)
            if st and st.accessible:
                status_text = f"[green]OK[/green] ({st.source_type})"
            elif st:
                status_text = f"[red]Error[/red]: {st.error}"
            else:
                status_text = "[yellow]Unknown[/yellow]"

            table.add_row(
                r.name,
                r.source,
                st.source_type if st else "?",
                r.use_when,
                status_text,
            )

        console.print(table)

    elif action == "add":
        if not source:
            console.print("[red]Error:[/red] Source path is required.")
            console.print(
                "Usage: [cyan]co ref add <path|git:owner/repo[@ref]> "
                "--name <name>[/cyan]"
            )
            raise typer.Exit(1)

        ref_name = name
        if not ref_name:
            if is_remote_source(source):
                from cokodo_agent.remote import parse_remote_source
                try:
                    parsed = parse_remote_source(source)
                    ref_name = parsed.repo
                except ValueError:
                    ref_name = source.split("/")[-1].split("@")[0]
            else:
                source_path = Path(source)
                ref_name = (
                    source_path.stem if source_path.is_file()
                    else source_path.name
                )
                if ref_name == ".agent":
                    ref_name = source_path.parent.name

        scope_list: list[str] | None = None
        if scope:
            scope_list = [s.strip() for s in scope.split(",") if s.strip()]

        try:
            add_reference_to_manifest(
                agent_dir,
                name=ref_name,
                source=source,
                description=description or "",
                scope=scope_list,
                use_when=use_when or "on_demand",
            )
        except ValueError as e:
            console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1)

        if is_remote_source(source):
            console.print(
                f"[green]Added[/green] reference [bold]{ref_name}[/bold] "
                f"-> {source} (remote)"
            )
            console.print(
                "  Run [cyan]co ref fetch[/cyan] to download and cache."
            )
        else:
            resolved = (project_root / source).resolve()
            stype = detect_source_type(resolved)
            console.print(
                f"[green]Added[/green] reference [bold]{ref_name}[/bold] "
                f"-> {source} ({stype})"
            )

    elif action == "remove":
        if not source:
            console.print("[red]Error:[/red] Reference name is required.")
            console.print("Usage: [cyan]co ref remove <name>[/cyan]")
            raise typer.Exit(1)

        removed = remove_reference_from_manifest(agent_dir, source)
        if removed:
            console.print(
                f"[green]Removed[/green] reference [bold]{source}[/bold]"
            )
        else:
            console.print(f"[red]Error:[/red] Reference '{source}' not found.")
            raise typer.Exit(1)

    elif action == "check":
        refs = load_references(agent_dir)
        if not refs:
            console.print("No references to check.")
            return

        statuses = check_references(agent_dir, project_root)
        all_ok = True

        for st in statuses:
            if st.accessible:
                console.print(
                    f"  [green]OK[/green]   {st.name} -> {st.source_type} "
                    f"({st.resolved_path})"
                )
            else:
                console.print(
                    f"  [red]FAIL[/red] {st.name}: {st.error}"
                )
                all_ok = False

        console.print()
        if all_ok:
            console.print(
                f"[green]All {len(statuses)} references accessible.[/green]"
            )
        else:
            failed = sum(1 for s in statuses if not s.accessible)
            console.print(
                f"[red]{failed}/{len(statuses)} references failed.[/red]"
            )
            raise typer.Exit(1)

    elif action == "fetch":
        from cokodo_agent.remote import (
            fetch_remote_source,
            parse_remote_source,
        )

        refs = load_references(agent_dir)
        remote_refs = [r for r in refs if is_remote_source(r.source)]

        if not remote_refs:
            console.print("No remote references to fetch.")
            return

        for r in remote_refs:
            try:
                parsed = parse_remote_source(r.source)
                console.print(
                    f"  Fetching [bold]{r.name}[/bold] "
                    f"({parsed.owner}/{parsed.repo})...",
                    end="",
                )
                fetch_remote_source(parsed, force=True)
                console.print(" [green]OK[/green]")
            except (ConnectionError, FileNotFoundError, ValueError) as e:
                console.print(f" [red]FAIL[/red]: {e}")

    elif action == "cache":
        from cokodo_agent.remote import clear_cache, list_cached_sources

        if source == "clear":
            count = clear_cache()
            console.print(
                f"[green]Cleared {count} cached source(s).[/green]"
            )
        elif source == "list" or source is None:
            cached = list_cached_sources()
            if not cached:
                console.print("No cached remote sources.")
                return

            table = Table(title="Cached Remote Sources")
            table.add_column("Source", style="bold")
            table.add_column("Fetched")
            table.add_column("Fresh")
            table.add_column("Path", style="dim")

            for c in cached:
                fresh = "[green]yes[/green]" if c["fresh"] else "[yellow]stale[/yellow]"
                table.add_row(c["source"], c["fetched_iso"], fresh, c["path"])

            console.print(table)
        else:
            console.print(
                f"[red]Error:[/red] Unknown cache action '{source}'"
            )
            console.print("Usage: [cyan]co ref cache[/cyan] or "
                          "[cyan]co ref cache clear[/cyan]")
            raise typer.Exit(1)

    else:
        console.print(f"[red]Error:[/red] Unknown action '{action}'")
        console.print(
            "Valid actions: list, add, remove, check, fetch, cache"
        )
        raise typer.Exit(1)


# ---------------------------------------------------------------------------
# Cross-project: co collab
# ---------------------------------------------------------------------------


@app.command()
def collab(
    action: str = typer.Argument(
        ...,
        help="Action: list, add, remove, status, diff, pull",
    ),
    partner_or_name: Optional[str] = typer.Argument(
        None,
        help="Partner path (for add) or collaboration name (for remove/diff/pull)",
    ),
    path: Optional[Path] = typer.Option(
        None,
        "--path",
        "-p",
        help="Path to project (default: current directory)",
    ),
    name: Optional[str] = typer.Option(
        None,
        "--name",
        "-n",
        help="Collaboration name (for add; defaults to directory name)",
    ),
    description: Optional[str] = typer.Option(
        None,
        "--description",
        "-d",
        help="Collaboration description",
    ),
    role: Optional[str] = typer.Option(
        None,
        "--role",
        "-r",
        help="Role: master or replica (default: replica)",
    ),
    scope: Optional[str] = typer.Option(
        None,
        "--scope",
        "-s",
        help="Comma-separated shared scope patterns (for add)",
    ),
) -> None:
    """Manage collaborations with partner projects (master/replica model)."""
    from cokodo_agent.relations import (
        add_collaboration_to_manifest,
        check_collaborations,
        diff_collaboration,
        load_collaborations,
        pull_collaboration,
        remove_collaboration_from_manifest,
    )

    try:
        agent_dir = find_agent_dir(path)
    except FileNotFoundError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)

    project_root = agent_dir.parent

    if action == "list":
        collabs = load_collaborations(agent_dir)
        if not collabs:
            console.print("No collaborations declared.")
            console.print()
            console.print(
                "Add one with: [cyan]co collab add <partner-path> --name <name>[/cyan]"
            )
            return

        statuses = check_collaborations(agent_dir, project_root)
        status_map = {s.name: s for s in statuses}

        table = Table(title="Declared Collaborations")
        table.add_column("Name", style="bold")
        table.add_column("Partner")
        table.add_column("Role")
        table.add_column("Status")

        for c in collabs:
            st = status_map.get(c.name)
            if st and st.partner_accessible:
                if st.shared_files_in_sync:
                    status_text = "[green]In sync[/green]"
                else:
                    drift_count = len(st.drift_files) if st.drift_files else 0
                    status_text = f"[yellow]{drift_count} file(s) drifted[/yellow]"
            elif st:
                status_text = f"[red]Error[/red]: {st.error}"
            else:
                status_text = "[yellow]Unknown[/yellow]"

            table.add_row(c.name, c.partner, c.role, status_text)

        console.print(table)

    elif action == "add":
        if not partner_or_name:
            console.print("[red]Error:[/red] Partner path is required.")
            console.print(
                "Usage: [cyan]co collab add <partner-path> --name <name>[/cyan]"
            )
            raise typer.Exit(1)

        collab_name = name
        if not collab_name:
            partner_path = Path(partner_or_name)
            collab_name = partner_path.name
            if collab_name == ".agent":
                collab_name = partner_path.parent.name

        scope_list: list[str] | None = None
        if scope:
            scope_list = [s.strip() for s in scope.split(",") if s.strip()]

        try:
            add_collaboration_to_manifest(
                agent_dir,
                name=collab_name,
                partner=partner_or_name,
                role=role or "replica",
                description=description or "",
                shared_scope=scope_list,
            )
        except ValueError as e:
            console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1)

        console.print(
            f"[green]Added[/green] collaboration [bold]{collab_name}[/bold] "
            f"-> {partner_or_name} (role: {role or 'replica'})"
        )

    elif action == "remove":
        if not partner_or_name:
            console.print("[red]Error:[/red] Collaboration name is required.")
            console.print("Usage: [cyan]co collab remove <name>[/cyan]")
            raise typer.Exit(1)

        removed = remove_collaboration_from_manifest(agent_dir, partner_or_name)
        if removed:
            console.print(
                f"[green]Removed[/green] collaboration [bold]{partner_or_name}[/bold]"
            )
        else:
            console.print(
                f"[red]Error:[/red] Collaboration '{partner_or_name}' not found."
            )
            raise typer.Exit(1)

    elif action == "status":
        collabs = load_collaborations(agent_dir)
        if not collabs:
            console.print("No collaborations to check.")
            return

        statuses = check_collaborations(agent_dir, project_root)
        all_ok = True

        for st in statuses:
            if not st.partner_accessible:
                console.print(
                    f"  [red]FAIL[/red] {st.name} ({st.role}): {st.error}"
                )
                all_ok = False
            elif st.shared_files_in_sync:
                console.print(
                    f"  [green]OK[/green]   {st.name} ({st.role}): in sync"
                )
            else:
                drift_count = len(st.drift_files) if st.drift_files else 0
                console.print(
                    f"  [yellow]DRIFT[/yellow] {st.name} ({st.role}): "
                    f"{drift_count} file(s) differ"
                )
                if st.drift_files:
                    for f in st.drift_files:
                        console.print(f"         - {f}")
                all_ok = False

        console.print()
        if all_ok:
            console.print(
                f"[green]All {len(statuses)} collaborations in sync.[/green]"
            )
        else:
            console.print(
                "Run [cyan]co collab diff <name>[/cyan] to see details, or "
                "[cyan]co collab pull <name>[/cyan] to sync replica."
            )

    elif action == "diff":
        if not partner_or_name:
            console.print("[red]Error:[/red] Collaboration name is required.")
            console.print("Usage: [cyan]co collab diff <name>[/cyan]")
            raise typer.Exit(1)

        from cokodo_agent.relations import get_collaboration_by_name

        c = get_collaboration_by_name(agent_dir, partner_or_name)
        if not c:
            console.print(
                f"[red]Error:[/red] Collaboration '{partner_or_name}' not found."
            )
            raise typer.Exit(1)

        try:
            diffs = diff_collaboration(c, project_root, agent_dir)
        except FileNotFoundError as e:
            console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1)

        if not diffs:
            console.print(f"No shared-scope files found for '{partner_or_name}'.")
            return

        table = Table(title=f"Diff: {partner_or_name}")
        table.add_column("File", style="cyan")
        table.add_column("Status")

        status_styles = {
            "in_sync": "[green]in sync[/green]",
            "modified": "[yellow]modified[/yellow]",
            "partner_only": "[blue]partner only[/blue]",
            "local_only": "[dim]local only[/dim]",
        }

        for fpath, fstatus in diffs.items():
            table.add_row(fpath, status_styles.get(fstatus, fstatus))

        console.print(table)

        modified = sum(1 for s in diffs.values() if s != "in_sync")
        if modified == 0:
            console.print()
            console.print("[green]All files in sync.[/green]")
        else:
            console.print()
            console.print(f"[yellow]{modified} file(s) differ.[/yellow]")
            if c.role == "replica":
                console.print(
                    f"Run [cyan]co collab pull {partner_or_name}[/cyan] to sync."
                )

    elif action == "pull":
        if not partner_or_name:
            console.print("[red]Error:[/red] Collaboration name is required.")
            console.print("Usage: [cyan]co collab pull <name>[/cyan]")
            raise typer.Exit(1)

        from cokodo_agent.relations import get_collaboration_by_name

        c = get_collaboration_by_name(agent_dir, partner_or_name)
        if not c:
            console.print(
                f"[red]Error:[/red] Collaboration '{partner_or_name}' not found."
            )
            raise typer.Exit(1)

        try:
            updated = pull_collaboration(c, project_root, agent_dir)
        except (ValueError, FileNotFoundError) as e:
            console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1)

        if updated:
            console.print(
                f"[green]Pulled {len(updated)} file(s) from "
                f"'{partner_or_name}':[/green]"
            )
            for f in updated:
                console.print(f"  + {f}")
        else:
            console.print(f"Already in sync with '{partner_or_name}'.")

    else:
        console.print(f"[red]Error:[/red] Unknown action '{action}'")
        console.print("Valid actions: list, add, remove, status, diff, pull")
        raise typer.Exit(1)


# ---------------------------------------------------------------------------
# Global registry: co global
# ---------------------------------------------------------------------------

global_app = typer.Typer(
    name="global",
    help="Manage the local global project registry (~/.cokodo/)",
    add_completion=False,
)
app.add_typer(global_app, name="global")


@global_app.command("list")
def global_list(
    all_: bool = typer.Option(
        False,
        "--all",
        "-a",
        help="Include invalid (moved/deleted) entries",
    ),
    json_output: bool = typer.Option(
        False,
        "--json",
        help="Output as JSON",
    ),
) -> None:
    """List all registered cokodo projects on this machine."""
    import json as json_module

    from cokodo_agent.registry import list_projects

    projects = list_projects(include_invalid=all_)

    if json_output:
        print(json_module.dumps(projects, indent=2, ensure_ascii=False))
        return

    if not projects:
        console.print("[yellow]No projects registered yet.[/yellow]")
        console.print()
        console.print(
            "Projects are registered automatically when you run any [cyan]co[/cyan] command "
            "inside a directory that has [cyan].agent/[/cyan]."
        )
        return

    table = Table(title=f"Registered Projects ({len(projects)})")
    table.add_column("ID", style="dim", width=10)
    table.add_column("Name", style="bold", width=24)
    table.add_column("Stack", width=8)
    table.add_column("Status", width=36)
    table.add_column("Last Seen", width=12)
    table.add_column("Valid", width=6)

    for p in projects:
        last_seen = (p.get("last_seen") or "")[:10]
        valid = "[green]yes[/green]" if p.get("is_valid") else "[red]no[/red]"
        table.add_row(
            p["id"][:8],
            p["name"],
            p.get("stack") or "-",
            (p.get("status_summary") or "")[:36],
            last_seen,
            valid,
        )

    console.print()
    console.print(table)
    console.print()
    console.print(f"[dim]Registry: {COKODO_HOME_DIR / 'cokodo.db'}[/dim]")


@global_app.command("info")
def global_info(
    name_or_id: str = typer.Argument(..., help="Project name or ID"),
    json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
) -> None:
    """Show detailed info for a registered project."""
    import json as json_module

    from cokodo_agent.registry import get_project, get_snapshot

    project = get_project(name_or_id)
    if not project:
        console.print(f"[red]Error:[/red] Project '{name_or_id}' not found.")
        raise typer.Exit(1)

    if json_output:
        print(json_module.dumps(project, indent=2, ensure_ascii=False))
        return

    console.print()
    console.print(Panel.fit(
        f"[bold blue]{project['name']}[/bold blue]",
        border_style="blue",
    ))
    console.print()

    rows = [
        ("ID", project["id"]),
        ("Path", project["path"]),
        ("Stack", project.get("stack") or "-"),
        ("Protocol", project.get("protocol_version") or "-"),
        ("Agent version", project.get("agent_version") or "-"),
        ("Last seen", (project.get("last_seen") or "")[:19]),
        ("Registered", (project.get("created_at") or "")[:19]),
        ("Valid", "yes" if project.get("is_valid") else "no"),
    ]

    for label, value in rows:
        console.print(f"  [cyan]{label:16}[/cyan] {value}")

    status = get_snapshot(project["id"], "status_summary")
    if status:
        console.print()
        console.print(f"  [bold]Status:[/bold] {status}")

    tags_raw = project.get("tags") or "[]"
    try:
        import json as json_module
        tags = json_module.loads(tags_raw)
        if tags:
            console.print(f"  [bold]Tags:[/bold] {', '.join(tags)}")
    except Exception:
        pass

    console.print()


@global_app.command("status")
def global_status(
    json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
) -> None:
    """Show status summary for all registered projects."""
    import json as json_module

    from cokodo_agent.registry import list_projects

    projects = list_projects()

    if json_output:
        out = [
            {
                "id": p["id"],
                "name": p["name"],
                "path": p["path"],
                "status_summary": p.get("status_summary"),
                "last_seen": p.get("last_seen"),
            }
            for p in projects
        ]
        print(json_module.dumps(out, indent=2, ensure_ascii=False))
        return

    if not projects:
        console.print("[yellow]No projects registered.[/yellow]")
        return

    console.print()
    console.print(
        Panel.fit(
            f"[bold blue]Global Project Status Overview[/bold blue]\n"
            f"{len(projects)} project(s) registered",
            border_style="blue",
        )
    )
    console.print()

    for p in projects:
        name = p["name"]
        status = p.get("status_summary") or "[dim](no status)[/dim]"
        last = (p.get("last_seen") or "")[:10]
        console.print(f"  [bold cyan]{name}[/bold cyan]  [dim]{last}[/dim]")
        console.print(f"    {status}")
        console.print()


@global_app.command("search")
def global_search(
    keyword: str = typer.Argument(..., help="Keyword to search for"),
    json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
) -> None:
    """Search registered projects by name, status, or path."""
    import json as json_module

    from cokodo_agent.registry import search_projects

    results = search_projects(keyword)

    if json_output:
        print(json_module.dumps(results, indent=2, ensure_ascii=False))
        return

    if not results:
        console.print(f"[yellow]No projects matching '{keyword}'.[/yellow]")
        return

    table = Table(title=f"Search results for '{keyword}'")
    table.add_column("Name", style="bold", width=24)
    table.add_column("Stack", width=8)
    table.add_column("Status", width=40)
    table.add_column("Path", style="dim", width=40)

    for p in results:
        table.add_row(
            p["name"],
            p.get("stack") or "-",
            (p.get("status_summary") or "")[:40],
            p["path"],
        )

    console.print()
    console.print(table)


@global_app.command("gc")
def global_gc(
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
) -> None:
    """Remove registry entries for projects that no longer exist on disk."""
    from cokodo_agent.registry import gc_projects, list_projects

    # Preview
    all_projects = list_projects()
    to_invalidate = [p for p in all_projects if not Path(p["path"]).exists()]

    if not to_invalidate:
        console.print("[green]All registry entries are valid. Nothing to clean up.[/green]")
        return

    console.print(
        f"[yellow]{len(to_invalidate)} project(s) no longer exist on disk:[/yellow]"
    )
    for p in to_invalidate:
        console.print(f"  - {p['name']}  {p['path']}")
    console.print()

    if not yes:
        confirmed = typer.confirm("Mark these as invalid?", default=True)
        if not confirmed:
            console.print("[yellow]Cancelled.[/yellow]")
            raise typer.Exit(0)

    marked, deleted = gc_projects()
    console.print(f"[green]Done.[/green] {marked} marked invalid, {deleted} old entries deleted.")


@global_app.command("unregister")
def global_unregister(
    name_or_path: str = typer.Argument(
        ...,
        help="Project name, ID, or absolute path to remove",
    ),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
) -> None:
    """Manually remove a project from the registry."""
    from cokodo_agent.registry import get_project, unregister_project

    project = get_project(name_or_path)
    if not project:
        # Try by path
        try:
            from cokodo_agent.registry import _project_id
            p = Path(name_or_path).resolve()
            pid = _project_id(p)
            project = get_project(pid)
        except Exception:
            pass

    if not project:
        console.print(f"[red]Error:[/red] Project '{name_or_path}' not found in registry.")
        raise typer.Exit(1)

    console.print(f"Remove [bold]{project['name']}[/bold] ({project['path']}) from registry?")

    if not yes:
        confirmed = typer.confirm("Confirm?", default=False)
        if not confirmed:
            console.print("[yellow]Cancelled.[/yellow]")
            raise typer.Exit(0)

    ok = unregister_project(name_or_path)
    if ok:
        console.print(f"[green]Removed[/green] {project['name']}")
    else:
        console.print(f"[red]Error:[/red] Could not remove entry.")
        raise typer.Exit(1)


# ---------------------------------------------------------------------------
# Help data — module-level so both `help` command and error handler share it
# ---------------------------------------------------------------------------

_COMMANDS_INFO: dict[str, dict] = {
    "init": {
        "description": "Create .agent protocol in target directory",
        "usage": "co init [PATH] [OPTIONS]",
        "options": [
            ("-y, --yes", "Skip prompts, use defaults"),
            ("-n, --name", "Project name"),
            ("-s, --stack", "Tech stack (python/rust/qt/mixed/other)"),
            ("-t, --tools", "IDE adapters: cursor,claude,copilot,gemini,all (used with --yes)"),
            ("-f, --force", "Overwrite existing .agent directory"),
            ("--online", "Fetch latest protocol from GitHub Release (default: use bundled)"),
        ],
        "examples": [
            ("co init", "Initialize in current directory with prompts"),
            ("co init -y", "Initialize with defaults (protocol only, no adapters)"),
            ("co init -y --tools cursor", "Initialize with Cursor adapter"),
            ("co init -y --tools all", "Initialize with all IDE adapters"),
            ("co init ./myproject -n MyApp -s python", "Initialize with options"),
            ("co init --online", "Fetch latest protocol from GitHub before initializing"),
        ],
    },
    "lint": {
        "description": "Check protocol compliance against rules",
        "usage": "co lint [PATH] [OPTIONS]",
        "options": [
            ("-r, --rule", "Check specific rule only"),
            ("-f, --format", "Output format (text/json/github)"),
        ],
        "examples": [
            ("co lint", "Check current directory"),
            ("co lint -r integrity-violation", "Check specific rule"),
            ("co lint -f json", "Output as JSON"),
            ("co lint -f github", "Output for GitHub Actions"),
        ],
    },
    "diff": {
        "description": "Compare local .agent with latest protocol",
        "usage": "co diff [PATH] [OPTIONS]",
        "options": [
            ("--offline", "Use built-in protocol (no network)"),
        ],
        "examples": [
            ("co diff", "Show differences with latest"),
            ("co diff --offline", "Compare with built-in protocol"),
        ],
    },
    "sync": {
        "description": "Sync local .agent with latest protocol",
        "usage": "co sync [PATH] [OPTIONS]",
        "options": [
            ("--offline", "Use built-in protocol (no network)"),
            ("--dry-run", "Show what would be updated"),
            ("-y, --yes", "Skip confirmation prompt"),
        ],
        "examples": [
            ("co sync", "Sync with confirmation"),
            ("co sync -y", "Sync without confirmation"),
            ("co sync --dry-run", "Preview changes"),
        ],
    },
    "adapt": {
        "description": "Generate IDE entry file(s) from existing .agent",
        "usage": "co adapt <cursor|claude|copilot|gemini|all> [PATH]",
        "options": [],
        "examples": [
            ("co adapt cursor", "Generate .cursor/rules/agent-protocol.mdc + .cursor/mcp.json"),
            ("co adapt claude", "Generate CLAUDE.md + .mcp.json"),
            ("co adapt copilot", "Generate AGENTS.md + .vscode/mcp.json (VS Code / Copilot)"),
            ("co adapt gemini", "Generate GEMINI.md + mcp_config.json (Antigravity)"),
            ("co adapt all", "Generate all IDE adapter files and MCP configs"),
        ],
    },
    "detect": {
        "description": "Detect IDE instruction files in the project",
        "usage": "co detect [PATH]",
        "options": [],
        "examples": [
            ("co detect", "List detected CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules/"),
        ],
    },
    "import": {
        "description": "Import rules from IDE instruction files into .agent",
        "usage": "co import [PATH] [OPTIONS]",
        "options": [
            ("-s, --source", "cursor | claude | copilot | gemini | auto"),
            ("--dry-run", "Show what would be imported without writing"),
        ],
        "examples": [
            ("co import", "Import from all detected files"),
            ("co import --dry-run", "Preview import"),
            ("co import -s claude", "Import only from CLAUDE.md"),
        ],
    },
    "context": {
        "description": "Get context files based on stack and task type",
        "usage": "co context [PATH] [OPTIONS]",
        "options": [
            ("-s, --stack", "Tech stack (python/rust/qt/mixed)"),
            ("-t, --task", "Task type (coding/testing/review/...)"),
            ("-o, --output", "Output format (list/paths/content)"),
        ],
        "examples": [
            ("co context", "List all context files"),
            ("co context -s python", "Files for Python stack"),
            ("co context -t testing", "Files for testing task"),
            ("co context -o content", "Output file contents"),
        ],
    },
    "journal": {
        "description": "Record a session entry to session-journal.md",
        "usage": "co journal [PATH] [OPTIONS]",
        "options": [
            ("-t, --title", "Session title"),
            ("-c, --completed", "Completed items (comma-separated)"),
            ("-d, --debt", "Technical debt items"),
            ("--decisions", "Key decisions made"),
            ("-i, --interactive", "Interactive mode with prompts"),
        ],
        "examples": [
            ("co journal -i", "Interactive mode"),
            ('co journal -t "Feature X" -c "Task 1, Task 2"', "Quick entry"),
        ],
    },
    "scaffold": {
        "description": "Audit and scaffold missing project/ files from templates",
        "usage": "co scaffold [PATH] [OPTIONS]",
        "options": [
            ("-f, --force", "Overwrite existing files (re-create from template)"),
            ("-y, --yes", "Skip confirmation prompt"),
        ],
        "examples": [
            ("co scaffold", "Show audit table and create missing files"),
            ("co scaffold -y", "Create missing files without confirmation"),
            ("co scaffold --force -y", "Re-create ALL project files from templates"),
        ],
    },
    "status": {
        "description": "Show or initialize project development status",
        "usage": "co status [PATH] [OPTIONS]",
        "options": [
            ("--raw", "Show raw markdown content"),
            ("--json", "Output as JSON (for scripting)"),
            ("--init", "Create status.md from template if missing"),
        ],
        "examples": [
            ("co status", "Show formatted project status"),
            ("co status --raw", "Output raw markdown"),
            ("co status --json", "Output as JSON"),
            ("co status --init", "Create status.md if missing"),
        ],
    },
    "serve": {
        "description": "Start MCP server for IDE integration (requires \\[mcp] extra)",
        "usage": "co serve [PATH] [OPTIONS]",
        "options": [
            ("-t, --transport", "Transport mode (stdio/streamable-http)"),
            ("-w, --workspace", "Workspace mode: discover all .agent/ projects under path"),
            ("-g, --global", "Global mode: serve all projects from local registry"),
        ],
        "examples": [
            ("co serve", "Start stdio MCP server (Cursor, Claude, VS Code, Antigravity)"),
            ("co serve --workspace", "Start workspace server for all projects under cwd"),
            ("co serve --workspace ~/projects", "Serve all projects under ~/projects"),
            ("co serve --global", "Serve all registered projects (global registry)"),
            ("co serve -t streamable-http", "Start HTTP MCP server"),
            ("pip install cokodo-agent\\[mcp]", "Install MCP dependency first"),
        ],
    },
    "ref": {
        "description": "Manage references to other projects, documents, and remote repos",
        "usage": "co ref <list|add|remove|check|fetch|cache> [OPTIONS]",
        "options": [
            ("--name", "Reference name (for add)"),
            ("--description", "Reference description (for add)"),
            ("--scope", "Comma-separated scope patterns (for add)"),
            ("--use-when", "Loading strategy: always/session_start/on_demand (for add)"),
        ],
        "examples": [
            ("co ref list", "List all declared references and their status"),
            ("co ref add ../backend/.agent --name backend", "Add a project reference"),
            ("co ref add ../docs/api-spec.md --name api-spec", "Add a single-file reference"),
            ("co ref add git:owner/repo --name upstream", "Add a remote GitHub reference"),
            ("co ref add git:owner/repo@v2.0:docs --name docs", "Remote ref with branch and path"),
            ("co ref remove backend", "Remove a reference"),
            ("co ref check", "Verify all reference paths are accessible"),
            ("co ref fetch", "Fetch/refresh all remote references"),
            ("co ref cache", "List cached remote sources"),
            ("co ref cache clear", "Clear all cached remote sources"),
        ],
    },
    "collab": {
        "description": "Manage collaborations with partner projects (master/replica)",
        "usage": "co collab <list|add|remove|status|diff|pull> [OPTIONS]",
        "options": [
            ("--name", "Collaboration name (for add)"),
            ("--description", "Collaboration description (for add)"),
            ("--role", "Role: master or replica (for add; default: replica)"),
            ("--scope", "Comma-separated shared scope patterns (for add)"),
        ],
        "examples": [
            ("co collab list", "List all collaborations and their sync status"),
            ("co collab add ../backend/.agent --name backend --role replica", "Add as replica of backend"),
            ("co collab add ../frontend/.agent --name frontend --role master", "Add as master for frontend"),
            ("co collab remove backend", "Remove a collaboration"),
            ("co collab status", "Check sync status of all collaborations"),
            ("co collab diff backend", "Show file-by-file diff with partner"),
            ("co collab pull backend", "Pull shared files from master (replica only)"),
        ],
    },
    "global": {
        "description": "Manage local global project registry (~/.cokodo/)",
        "usage": "co global <list|info|status|search|gc|unregister>",
        "options": [],
        "examples": [
            ("co global list", "List all registered projects"),
            ("co global status", "Show status summary for all projects"),
            ("co global info <name>", "Show detailed info for a project"),
            ("co global search <keyword>", "Search projects by name, status, or path"),
            ("co global gc", "Remove entries for deleted/moved projects"),
            ("co global unregister <name>", "Manually remove a project from registry"),
        ],
    },
    "update-checksums": {
        "description": "Update checksums in manifest.json (maintainer only)",
        "usage": "co update-checksums [PATH]",
        "options": [],
        "examples": [
            ("co update-checksums", "Update checksums"),
        ],
    },
    "version": {
        "description": "Show version information",
        "usage": "co version",
        "options": [],
        "examples": [
            ("co version", "Show version"),
        ],
    },
    "help": {
        "description": "Show help information for commands",
        "usage": "co help [COMMAND]",
        "options": [],
        "examples": [
            ("co help", "Show all commands"),
            ("co help init", "Show help for init command"),
        ],
    },
}

_HELP_CATEGORIES: dict[str, list[str]] = {
    "Setup": ["init", "adapt", "detect", "import"],
    "Protocol Management": ["lint", "diff", "sync", "update-checksums"],
    "Development": ["status", "scaffold", "context", "journal"],
    "Cross-Project": ["ref", "collab"],
    "Global Registry": ["global"],
    "MCP Integration": ["serve"],
    "Information": ["version", "help"],
}


def _print_commands_overview() -> None:
    """Print the commands overview panel (shared by `help` and error handler)."""
    console.print()
    console.print(
        Panel.fit(
            f"[bold blue]Cokodo Agent[/bold blue] v{VERSION}\n"
            "AI Collaboration Protocol Generator",
            border_style="blue",
        )
    )
    console.print()
    console.print("[bold]Commands:[/bold]")
    console.print()

    for category, cmds in _HELP_CATEGORIES.items():
        console.print(f"  [bold cyan]{category}[/bold cyan]")
        for cmd in cmds:
            if cmd in _COMMANDS_INFO:
                desc = _COMMANDS_INFO[cmd]["description"]
                console.print(f"    [green]{cmd:18}[/green] {desc}")
        console.print()

    console.print("[bold]Usage:[/bold]")
    console.print("  co <command> [options]")
    console.print()
    console.print("[bold]Get help for a command:[/bold]")
    console.print("  co help <command>")
    console.print()
    console.print("[bold]Quick start:[/bold]")
    console.print("  co init          # Create .agent in current directory")
    console.print("  co lint          # Check protocol compliance")
    console.print("  co sync          # Update to latest protocol")
    console.print()


@app.command()
def help(
    command: Optional[str] = typer.Argument(
        None,
        help="Command to get help for",
    ),
) -> None:
    """Show help information for commands."""
    if command:
        if command not in _COMMANDS_INFO:
            console.print(f"[red]Error:[/red] Unknown command '{command}'")
            console.print()
            _print_commands_overview()
            raise typer.Exit(1)

        info = _COMMANDS_INFO[command]
        console.print()
        console.print(Panel.fit(f"[bold blue]co {command}[/bold blue]", border_style="blue"))
        console.print()
        console.print(f"[bold]Description:[/bold] {info['description']}")
        console.print()
        console.print(f"[bold]Usage:[/bold] {info['usage']}")

        if info["options"]:
            console.print()
            console.print("[bold]Options:[/bold]")
            for opt, desc in info["options"]:
                console.print(f"  [cyan]{opt:20}[/cyan] {desc}")

        console.print()
        console.print("[bold]Examples:[/bold]")
        for example, desc in info["examples"]:
            console.print(f"  [green]{example}[/green]")
            console.print(f"    {desc}")
    else:
        _print_commands_overview()


def main() -> None:
    """CLI entry point with user-friendly unknown-command handling."""
    import re
    import sys

    import click

    try:
        app(standalone_mode=False)
    except click.exceptions.UsageError as e:
        msg = str(e)
        m = re.search(r"No such command ['\"](.+)['\"]", msg)
        if m:
            cmd_name = m.group(1)
            console.print(f"\n[red]Error:[/red] Unknown command [bold]'{cmd_name}'[/bold]")
            console.print()
            _print_commands_overview()
        else:
            console.print(f"[red]Error:[/red] {e}")
        sys.exit(2)
    except click.exceptions.Abort:
        sys.exit(1)
    except SystemExit:
        raise


if __name__ == "__main__":
    main()
