"""CLI entry point for code-review-graph.

Usage:
    code-review-graph install
    code-review-graph init
    code-review-graph build [--base BASE]
    code-review-graph update [--base BASE]
    code-review-graph watch
    code-review-graph status
    code-review-graph serve [--auto-watch] [--http] [--host ADDR] [--port PORT]
    code-review-graph mcp [--auto-watch]
    code-review-graph visualize
    code-review-graph wiki
    code-review-graph detect-changes [--base BASE] [--brief]
    code-review-graph register <path> [--alias name]
    code-review-graph unregister <path_or_alias>
    code-review-graph repos
    code-review-graph daemon start [--foreground]
    code-review-graph daemon stop
    code-review-graph daemon restart [--foreground]
    code-review-graph daemon status
    code-review-graph daemon logs [--repo ALIAS] [-f] [-n N]
    code-review-graph daemon add <path> [--alias NAME]
    code-review-graph daemon remove <path_or_alias>
"""

from __future__ import annotations

import sys

# Python version check — must come before any other imports
if sys.version_info < (3, 10):
    print("code-review-graph requires Python 3.10 or higher.")
    print(f"  You are running Python {sys.version}")
    print()
    print("Install Python 3.10+: https://www.python.org/downloads/")
    sys.exit(1)

import argparse
import json
import logging
import os
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as pkg_version
from pathlib import Path

logger = logging.getLogger(__name__)

# Shared platform choices for install and init commands
_PLATFORM_CHOICES = [
    "codex", "claude", "claude-code", "cursor", "windsurf", "zed",
    "continue", "opencode", "antigravity", "gemini-cli", "qwen", "kiro", "qoder",
    "copilot", "copilot-cli", "all",
]


def _get_version() -> str:
    """Get the installed package version.

    Tries ``importlib.metadata`` first (canonical source from the installed
    dist-info), falling back to the package's ``__version__`` attribute if
    metadata is unavailable or corrupt. This matters for editable installs
    on filesystems where iCloud / OneDrive can leave orphan dist-info dirs
    behind that confuse importlib.metadata's lookup.
    """
    try:
        v = pkg_version("code-review-graph")
        if v:
            return v
    except PackageNotFoundError as exc:
        logger.debug("Package metadata unavailable: %s", exc)
    # Fallback: read __version__ directly from the package.
    try:
        from . import __version__ as fallback_version
        if fallback_version:
            return fallback_version
    except ImportError:
        pass
    return "dev"


def _supports_color() -> bool:
    """Check if the terminal likely supports ANSI colors."""
    if os.environ.get("NO_COLOR"):
        return False
    if not hasattr(sys.stdout, "isatty"):
        return False
    return sys.stdout.isatty()


def _print_banner() -> None:
    """Print the startup banner with graph art and available commands."""
    color = _supports_color()
    version = _get_version()

    # ANSI escape codes
    c = "\033[36m" if color else ""  # cyan — graph art
    y = "\033[33m" if color else ""  # yellow — center node
    b = "\033[1m" if color else ""  # bold
    d = "\033[2m" if color else ""  # dim
    g = "\033[32m" if color else ""  # green — commands
    r = "\033[0m" if color else ""  # reset

    print(f"""
{c}  ●──●──●{r}
{c}  │╲ │ ╱│{r}       {b}code-review-graph{r}  {d}v{version}{r}
{c}  ●──{y}◆{c}──●{r}
{c}  │╱ │ ╲│{r}       {d}Structural knowledge graph for{r}
{c}  ●──●──●{r}       {d}smarter code reviews{r}

  {b}Commands:{r}
    {g}install{r}     Set up MCP server for AI coding platforms
    {g}init{r}        Alias for install
    {g}build{r}       Full graph build {d}(parse all files){r}
    {g}update{r}      Incremental update {d}(changed files only){r}
    {g}watch{r}       Auto-update on file changes
    {g}status{r}      Show graph statistics
    {g}visualize{r}   Generate interactive HTML graph
    {g}wiki{r}        Generate markdown wiki from communities
    {g}detect-changes{r} Analyze change impact {d}(risk-scored review){r}
    {g}register{r}    Register a repository in the multi-repo registry
    {g}unregister{r}  Remove a repository from the registry
    {g}repos{r}       List registered repositories
    {g}postprocess{r} Run post-processing {d}(flows, communities, FTS){r}
    {g}daemon{r}      Multi-repo watch daemon management
    {g}eval{r}        Run evaluation benchmarks
    {g}serve{r}       Start MCP server {d}(stdio, or {g}--http{r} on localhost:5555){r}

  {d}Run{r} {b}code-review-graph <command> --help{r} {d}for details{r}
""")


def _instruction_files_to_modify(
    repo_root: Path,
    target: str,
) -> list[str]:
    """Return the list of instruction files that ``install`` would write
    or modify, given the current state of the repo and the selected
    platform target. Used for the dry-run / confirm preview (#173).
    """
    from .skills import _CLAUDE_MD_SECTION_MARKER, _PLATFORM_INSTRUCTION_FILES

    targets: list[str] = []

    if target in ("claude", "all"):
        claude_md = repo_root / "CLAUDE.md"
        if claude_md.exists():
            content = claude_md.read_text(encoding="utf-8")
            if _CLAUDE_MD_SECTION_MARKER not in content:
                targets.append("CLAUDE.md (append)")
        else:
            targets.append("CLAUDE.md (new)")

    for filename, owners in _PLATFORM_INSTRUCTION_FILES.items():
        if target != "all" and target not in owners:
            continue
        path = repo_root / filename
        if path.exists():
            content = path.read_text(encoding="utf-8")
            if _CLAUDE_MD_SECTION_MARKER not in content:
                targets.append(f"{filename} (append)")
        else:
            targets.append(f"{filename} (new)")

    return targets


def _confirm_yes_no(prompt: str, default_yes: bool = True) -> bool:
    """Prompt the user [Y/n] and return True for yes.

    Non-interactive environments (no TTY on stdin, e.g. an MCP wrapper
    piping the CLI) return ``default_yes`` without blocking — the
    stdio transport cannot safely read from stdin without corrupting
    the JSON-RPC stream. See: #173, #174
    """
    if not sys.stdin.isatty():
        return default_yes
    suffix = "[Y/n]" if default_yes else "[y/N]"
    try:
        answer = input(f"{prompt} {suffix} ").strip().lower()
    except (EOFError, KeyboardInterrupt):
        print()
        return False
    if not answer:
        return default_yes
    return answer in ("y", "yes")


def _handle_init(args: argparse.Namespace) -> None:
    """Set up MCP config for detected AI coding platforms."""
    from .incremental import ensure_repo_gitignore_excludes_crg, find_repo_root
    from .skills import install_platform_configs

    repo_root = Path(args.repo) if args.repo else find_repo_root()
    if not repo_root:
        repo_root = Path.cwd()

    dry_run = getattr(args, "dry_run", False)
    target = getattr(args, "platform", "all") or "all"
    if target == "claude-code":
        target = "claude"
    auto_yes = getattr(args, "yes", False)
    skip_instructions = getattr(args, "no_instructions", False)

    print("Installing MCP server config...")
    configured = install_platform_configs(repo_root, target=target, dry_run=dry_run)

    if not configured:
        print("No platforms detected.")
    else:
        print(f"\nConfigured {len(configured)} platform(s): {', '.join(configured)}")

    # Preview the instruction files that would be touched (#173).
    instr_targets = _instruction_files_to_modify(repo_root, target)
    if instr_targets:
        print()
        print("Graph instructions will be injected into:")
        for t in instr_targets:
            print(f"  {t}")

    if dry_run:
        print("\n[dry-run] Would ensure .gitignore ignores .code-review-graph/.")
        print("[dry-run] No files were modified.")
        return

    gitignore_state = ensure_repo_gitignore_excludes_crg(repo_root)
    if gitignore_state == "created":
        print("Created .gitignore and added .code-review-graph/.")
    elif gitignore_state == "updated":
        print("Updated .gitignore with .code-review-graph/.")
    else:
        print(".gitignore already contains .code-review-graph/.")

    # Platform-native skills and hooks are installed by default where supported
    # so the graph tools are used proactively. Use --no-skills / --no-hooks /
    # --no-instructions to opt out.
    skip_skills = getattr(args, "no_skills", False)
    skip_hooks = getattr(args, "no_hooks", False)
    # Legacy: --skills/--hooks/--all still accepted (no-op, everything is default)

    from .skills import (
        PLATFORMS,
        generate_skills,
        inject_claude_md,
        inject_platform_instructions,
        install_codex_hooks,
        install_cursor_hooks,
        install_gemini_cli_hooks,
        install_gemini_cli_skills,
        install_git_hook,
        install_hooks,
        install_opencode_plugin,
        install_qoder_skills,
    )

    if not skip_skills:
        # Claude Code skills are only relevant for Claude (or full install).
        if target in ("claude", "all"):
            skills_dir = generate_skills(repo_root)
            print(f"Generated Claude Code skills in {skills_dir}")

        # Gemini CLI skills are workspace-scoped under .gemini/.
        if target in ("gemini-cli", "all"):
            gemini_skills_dir = install_gemini_cli_skills(repo_root)
            print(f"Installed Gemini CLI skills in {gemini_skills_dir}")

    # Confirm before writing instruction files (#173). --yes skips the
    # prompt; --no-instructions skips the whole block.
    if not skip_instructions and instr_targets:
        if auto_yes or _confirm_yes_no(
            "Inject graph instructions into the files above?",
            default_yes=True,
        ):
            if target in ("claude", "all"):
                inject_claude_md(repo_root)
            inject_platform_instructions(repo_root, target=target)
            # Use the precomputed instr_targets list for the confirmation
            # message; we don't need the fresh return value from
            # inject_platform_instructions here.
            names = [t.split(" ")[0] for t in instr_targets]
            print(f"Injected graph instructions into: {', '.join(names)}")
        else:
            print("Skipped instruction injection (user declined).")
    elif skip_instructions:
        print("Skipped instruction injection (--no-instructions).")


    # Install Qoder skills (global user-level skills directory)
    if not skip_skills and target in ("qoder", "all"):
        qoder_skills_dir = install_qoder_skills(repo_root)
        if qoder_skills_dir:
            print(f"Installed Qoder skills to {qoder_skills_dir}")
    if not skip_hooks and target in ("codex", "all"):
        hooks_path = install_codex_hooks(repo_root)
        print(f"Installed Codex hooks in {hooks_path}")
        git_hook = install_git_hook(repo_root)
        if git_hook:
            print(f"Installed git pre-commit hook in {git_hook}")
    if not skip_hooks and target in ("claude", "qoder", "all"):
        platforms_to_install = [target] if target != "all" else ["claude", "qoder"]
        for plat in platforms_to_install:
            install_hooks(repo_root, platform=plat)
            print(f"Installed hooks in {repo_root / f'.{plat}' / 'settings.json'}")
        git_hook = install_git_hook(repo_root)
        if git_hook:
            print(f"Installed git pre-commit hook in {git_hook}")

    # Cursor hooks (user-level, only if ~/.cursor exists — matching MCP detect)
    if not skip_hooks and target in ("all", "cursor") and PLATFORMS["cursor"]["detect"]():
        try:
            hooks_path = install_cursor_hooks()
            print(f"Installed Cursor hooks in {hooks_path}")
        except Exception as exc:
            logger.warning("Could not install Cursor hooks: %s", exc)

    if not skip_hooks and target in ("gemini-cli", "all"):
        try:
            gemini_settings = install_gemini_cli_hooks(repo_root)
            print(f"Installed Gemini CLI hooks in {gemini_settings}")
        except Exception as exc:
            logger.warning("Could not install Gemini CLI hooks: %s", exc)

    # OpenCode plugin (user-level, gated by same detect() as MCP config)
    if not skip_hooks and target in ("all", "opencode") and PLATFORMS["opencode"]["detect"]():
        try:
            plugin_path = install_opencode_plugin()
            print(f"Installed OpenCode plugin in {plugin_path}")
        except Exception as exc:
            logger.warning("Could not install OpenCode plugin: %s", exc)

    print()
    print("Next steps:")
    print("  1. code-review-graph build    # build the knowledge graph")
    print("  2. Restart your AI coding tool to pick up the new config")


def _handle_data_dir_option(args, repo_root: Path) -> None:
    """Handle --data-dir option by updating registry if specified."""
    if hasattr(args, "data_dir") and args.data_dir:
        try:
            from .registry import Registry
            data_dir_path = Path(args.data_dir).expanduser().resolve()
            data_dir_path.mkdir(parents=True, exist_ok=True)
            Registry().set_data_dir(str(repo_root), str(data_dir_path))
            logging.info(f"Graph database will be stored at: {data_dir_path}")
        except Exception as exc:
            logging.error(f"Failed to set data directory: {exc}")
            sys.exit(1)


def main() -> None:
    """Main CLI entry point."""
    ap = argparse.ArgumentParser(
        prog="code-review-graph",
        description="Persistent incremental knowledge graph for code reviews",
    )
    ap.add_argument("-v", "--version", action="store_true", help="Show version and exit")
    sub = ap.add_subparsers(dest="command")

    # install (primary) + init (alias)
    install_cmd = sub.add_parser("install", help="Register MCP server with AI coding platforms")
    install_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    install_cmd.add_argument(
        "--dry-run",
        action="store_true",
        help="Show what would be done without writing files",
    )
    install_cmd.add_argument(
        "--no-skills",
        action="store_true",
        help="Skip generating platform-native skill files",
    )
    install_cmd.add_argument(
        "--no-hooks",
        action="store_true",
        help="Skip installing platform-native hooks",
    )
    install_cmd.add_argument(
        "--no-instructions",
        action="store_true",
        help="Skip injecting graph instructions into CLAUDE.md / AGENTS.md / etc.",
    )
    install_cmd.add_argument(
        "-y",
        "--yes",
        action="store_true",
        help="Auto-confirm instruction injection without an interactive prompt",
    )
    # Legacy flags (kept for backwards compat, now no-ops since all is default)
    install_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS)
    install_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS)
    install_cmd.add_argument(
        "--all", action="store_true", dest="install_all", help=argparse.SUPPRESS
    )
    install_cmd.add_argument(
        "--platform",
        choices=_PLATFORM_CHOICES,
        default="all",
        help="Target platform for MCP config (default: all detected)",
    )

    init_cmd = sub.add_parser("init", help="Alias for install")
    init_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    init_cmd.add_argument(
        "--dry-run",
        action="store_true",
        help="Show what would be done without writing files",
    )
    init_cmd.add_argument(
        "--no-skills",
        action="store_true",
        help="Skip generating platform-native skill files",
    )
    init_cmd.add_argument(
        "--no-hooks",
        action="store_true",
        help="Skip installing platform-native hooks",
    )
    init_cmd.add_argument(
        "--no-instructions",
        action="store_true",
        help="Skip injecting graph instructions into CLAUDE.md / AGENTS.md / etc.",
    )
    init_cmd.add_argument(
        "-y",
        "--yes",
        action="store_true",
        help="Auto-confirm instruction injection without an interactive prompt",
    )
    init_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS)
    init_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS)
    init_cmd.add_argument("--all", action="store_true", dest="install_all", help=argparse.SUPPRESS)
    init_cmd.add_argument(
        "--platform",
        choices=_PLATFORM_CHOICES,
        default="all",
        help="Target platform for MCP config (default: all detected)",
    )

    # build
    build_cmd = sub.add_parser("build", help="Full graph build (re-parse all files)")
    build_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    build_cmd.add_argument(
        "--skip-flows",
        action="store_true",
        help="Skip flow/community detection (signatures + FTS only)",
    )
    build_cmd.add_argument(
        "--skip-postprocess",
        action="store_true",
        help="Skip all post-processing (raw parse only)",
    )
    build_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # update
    update_cmd = sub.add_parser("update", help="Incremental update (only changed files)")
    update_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)")
    update_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    update_cmd.add_argument(
        "--skip-flows",
        action="store_true",
        help="Skip flow/community detection (signatures + FTS only)",
    )
    update_cmd.add_argument(
        "--skip-postprocess",
        action="store_true",
        help="Skip all post-processing (raw parse only)",
    )
    update_cmd.add_argument(
        "--brief",
        action="store_true",
        help="After re-parsing changed files into the graph, also print the "
             "risk summary + Token Savings panel that 'detect-changes --brief' "
             "prints. Use this after a rebase or large change set when you "
             "want to refresh the graph AND see the impact in one command; "
             "use 'detect-changes --brief' alone when the graph is already "
             "up to date (analysis only, no re-parse).",
    )
    update_cmd.add_argument(
        "--verify",
        action="store_true",
        help="Calibrate the estimated savings against tiktoken's "
             "cl100k_base tokenizer (the GPT-4 family tokenizer). Adds a "
             "second row to the panel with the real token counts. Requires "
             "`pip install tiktoken`.",
    )
    update_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # postprocess
    pp_cmd = sub.add_parser(
        "postprocess",
        help="Run post-processing on existing graph (flows, communities, FTS)",
    )
    pp_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    pp_cmd.add_argument("--no-flows", action="store_true", help="Skip flow detection")
    pp_cmd.add_argument("--no-communities", action="store_true", help="Skip community detection")
    pp_cmd.add_argument("--no-fts", action="store_true", help="Skip FTS rebuild")
    pp_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # embed
    embed_cmd = sub.add_parser(
        "embed",
        help="Compute vector embeddings for semantic search",
    )
    embed_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    embed_cmd.add_argument(
        "--provider",
        choices=["local", "openai", "google", "minimax"],
        default=None,
        help="Embedding provider (default: local, needs code-review-graph[embeddings])",
    )
    embed_cmd.add_argument(
        "--model",
        default=None,
        help="Embedding model. For local: HuggingFace ID (default all-MiniLM-L6-v2); "
             "for openai/google/minimax: provider-specific model ID.",
    )
    embed_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # watch
    watch_cmd = sub.add_parser("watch", help="Watch for changes and auto-update")
    watch_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    watch_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # status
    status_cmd = sub.add_parser("status", help="Show graph statistics")
    status_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    status_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # visualize
    vis_cmd = sub.add_parser("visualize", help="Generate interactive HTML graph visualization")
    vis_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    vis_cmd.add_argument(
        "--mode",
        choices=["auto", "full", "community", "file"],
        default="auto",
        help="Rendering mode: auto (default), full, community, or file",
    )
    vis_cmd.add_argument(
        "--serve",
        action="store_true",
        help="Start a local HTTP server to view the visualization (localhost:8765)",
    )
    vis_cmd.add_argument(
        "--format",
        choices=["html", "graphml", "cypher", "obsidian", "svg"],
        default="html",
        help="Export format (default: html)",
    )
    vis_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # wiki
    wiki_cmd = sub.add_parser("wiki", help="Generate markdown wiki from community structure")
    wiki_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    wiki_cmd.add_argument(
        "--force",
        action="store_true",
        help="Regenerate all pages even if content unchanged",
    )
    wiki_cmd.add_argument(
        "--data-dir",
        default=None,
        help="External directory to store graph database (useful for network shares)"
    )

    # register
    register_cmd = sub.add_parser(
        "register", help="Register a repository in the multi-repo registry"
    )
    register_cmd.add_argument("path", help="Path to the repository root")
    register_cmd.add_argument("--alias", default=None, help="Short alias for the repository")

    # unregister
    unregister_cmd = sub.add_parser(
        "unregister", help="Remove a repository from the multi-repo registry"
    )
    unregister_cmd.add_argument("path_or_alias", help="Repository path or alias to remove")

    # repos
    sub.add_parser("repos", help="List registered repositories")

    # eval
    eval_cmd = sub.add_parser("eval", help="Run evaluation benchmarks")
    eval_cmd.add_argument(
        "--benchmark",
        default=None,
        help="Comma-separated benchmarks to run (token_efficiency, impact_accuracy, "
        "flow_completeness, search_quality, build_performance)",
    )
    eval_cmd.add_argument("--repo", default=None, help="Comma-separated repo config names")
    eval_cmd.add_argument("--all", action="store_true", dest="run_all", help="Run all benchmarks")
    eval_cmd.add_argument("--report", action="store_true", help="Generate report from results")
    eval_cmd.add_argument("--output-dir", default=None, help="Output directory for results")

    # detect-changes
    detect_cmd = sub.add_parser(
        "detect-changes",
        help="Analyze change impact against the existing graph (read-only). "
             "Does NOT re-parse files — for that, use 'update --brief'.",
    )
    detect_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)")
    detect_cmd.add_argument(
        "--brief",
        action="store_true",
        help="Show the risk summary + Token Savings panel instead of the "
             "full JSON. Read-only against the existing graph.",
    )
    detect_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    detect_cmd.add_argument(
        "--verify",
        action="store_true",
        help="Calibrate the estimated savings against tiktoken's "
             "cl100k_base tokenizer (the GPT-4 family tokenizer). Adds a "
             "second row to the panel with the real token counts. Requires "
             "`pip install tiktoken`.",
    )

    # serve / mcp
    serve_cmd = sub.add_parser(
        "serve",
        help="Start MCP server (stdio by default, or HTTP on localhost with --http)",
    )
    serve_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    serve_cmd.add_argument(
        "--auto-watch",
        action="store_true",
        help="Start filesystem watch in a daemon thread while MCP server runs",
    )
    serve_cmd.add_argument(
        "--tools", default=None,
        help=(
            "Comma-separated list of tool names to expose "
            "(e.g. query_graph_tool,semantic_search_nodes_tool). "
            "Unlisted tools are removed. Falls back to CRG_TOOLS env var. "
            "When unset, all tools are available."
        ),
    )
    serve_cmd.add_argument(
        "--http",
        action="store_true",
        help="Listen for MCP over Streamable HTTP on localhost (default port 5555)",
    )
    serve_cmd.add_argument(
        "--host",
        default=None,
        metavar="ADDR",
        help="Bind address for --http (default: 127.0.0.1)",
    )
    serve_cmd.add_argument(
        "--port",
        type=int,
        default=None,
        metavar="PORT",
        help="Port for --http (default: 5555)",
    )

    mcp_cmd = sub.add_parser("mcp", help="Alias for serve")
    mcp_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
    mcp_cmd.add_argument(
        "--auto-watch",
        action="store_true",
        help="Start filesystem watch in a daemon thread while MCP server runs",
    )

    # daemon
    daemon_cmd = sub.add_parser(
        "daemon",
        help="Multi-repo watch daemon (start/stop/status/add/remove)",
    )
    daemon_sub = daemon_cmd.add_subparsers(dest="daemon_command")

    daemon_start = daemon_sub.add_parser(
        "start",
        help="Start the watch daemon",
    )
    daemon_start.add_argument(
        "--foreground",
        action="store_true",
        help="Run in foreground instead of daemonizing",
    )

    daemon_sub.add_parser(
        "stop",
        help="Stop the watch daemon",
    )

    daemon_restart = daemon_sub.add_parser(
        "restart",
        help="Restart the watch daemon",
    )
    daemon_restart.add_argument(
        "--foreground",
        action="store_true",
        help="Run in foreground instead of daemonizing",
    )

    daemon_sub.add_parser("status", help="Show daemon and watcher status")

    daemon_logs = daemon_sub.add_parser(
        "logs",
        help="View daemon or watcher logs",
    )
    daemon_logs.add_argument(
        "--repo",
        default=None,
        help="Show logs for a specific repo alias",
    )
    daemon_logs.add_argument(
        "--follow",
        action="store_true",
        help="Follow log output (tail -f)",
    )
    daemon_logs.add_argument(
        "--lines",
        type=int,
        default=50,
        help="Number of lines to show (default: 50)",
    )

    daemon_add = daemon_sub.add_parser(
        "add",
        help="Add a repo to the watch config",
    )
    daemon_add.add_argument("path", help="Path to the repository")
    daemon_add.add_argument(
        "--alias",
        default=None,
        help="Short alias for the repo",
    )

    daemon_remove = daemon_sub.add_parser(
        "remove",
        help="Remove a repo from the watch config",
    )
    daemon_remove.add_argument(
        "path_or_alias",
        help="Repository path or alias to remove",
    )

    args = ap.parse_args()

    if args.version:
        print(f"code-review-graph {_get_version()}")
        return

    if not args.command:
        _print_banner()
        return

    if args.command in ("serve", "mcp"):
        from .main import main as serve_main

        auto_watch = getattr(args, "auto_watch", False)
        if args.command == "serve":
            if args.port is not None and not args.http:
                serve_cmd.error("--port requires --http")
            if args.host is not None and not args.http:
                serve_cmd.error("--host requires --http")
            if args.http:
                host = args.host if args.host is not None else "127.0.0.1"
                port = args.port if args.port is not None else 5555
                serve_main(
                    repo_root=args.repo,
                    auto_watch=auto_watch,
                    transport="streamable-http",
                    host=host,
                    port=port,
                    tools=args.tools,
                )
            else:
                serve_main(repo_root=args.repo, auto_watch=auto_watch, tools=args.tools)
        else:
            serve_main(repo_root=args.repo, auto_watch=auto_watch)
        return

    if args.command == "daemon":
        if not args.daemon_command:
            daemon_cmd.print_help()
            return
        from .daemon_cli import (
            _handle_add,
            _handle_logs,
            _handle_remove,
            _handle_restart,
            _handle_start,
            _handle_status,
            _handle_stop,
        )

        handlers = {
            "start": _handle_start,
            "stop": _handle_stop,
            "restart": _handle_restart,
            "status": _handle_status,
            "logs": _handle_logs,
            "add": _handle_add,
            "remove": _handle_remove,
        }
        handler = handlers.get(args.daemon_command)
        if handler:
            handler(args)
        return

    if args.command == "eval":
        from .eval.reporter import generate_full_report, generate_readme_tables
        from .eval.runner import run_eval

        if getattr(args, "report", False):
            output_dir = Path(getattr(args, "output_dir", None) or "evaluate/results")
            report = generate_full_report(output_dir)
            report_path = Path("evaluate/reports/summary.md")
            report_path.parent.mkdir(parents=True, exist_ok=True)
            report_path.write_text(report, encoding="utf-8")
            print(f"Report written to {report_path}")

            tables = generate_readme_tables(output_dir)
            print("\n--- README Tables (copy-paste) ---\n")
            print(tables)
        else:
            repos = (
                [r.strip() for r in args.repo.split(",")] if getattr(args, "repo", None) else None
            )
            benchmarks = (
                [b.strip() for b in args.benchmark.split(",")]
                if getattr(args, "benchmark", None)
                else None
            )

            if not repos and not benchmarks and not getattr(args, "run_all", False):
                print("Specify --all, --repo, or --benchmark. See --help.")
                return

            results = run_eval(
                repos=repos,
                benchmarks=benchmarks,
                output_dir=getattr(args, "output_dir", None),
            )
            print(f"\nCompleted {len(results)} benchmark(s).")
            print("Run 'code-review-graph eval --report' to generate tables.")
        return

    if args.command in ("init", "install"):
        _handle_init(args)
        return

    if args.command in ("register", "unregister", "repos"):
        logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
        from .registry import Registry

        registry = Registry()
        if args.command == "register":
            try:
                entry = registry.register(args.path, alias=args.alias)
                alias_info = f" (alias: {entry['alias']})" if entry.get("alias") else ""
                print(f"Registered: {entry['path']}{alias_info}")
            except ValueError as exc:
                logging.error(str(exc))
                sys.exit(1)
        elif args.command == "unregister":
            if registry.unregister(args.path_or_alias):
                print(f"Unregistered: {args.path_or_alias}")
            else:
                print(f"Not found: {args.path_or_alias}")
                sys.exit(1)
        elif args.command == "repos":
            repos = registry.list_repos()
            if not repos:
                print("No repositories registered.")
                print("Use: code-review-graph register <path> [--alias name]")
            else:
                for entry in repos:
                    alias = entry.get("alias", "")
                    alias_str = f"  ({alias})" if alias else ""
                    print(f"  {entry['path']}{alias_str}")
        return

    logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")

    from .graph import GraphStore
    from .incremental import (
        find_project_root,
        find_repo_root,
        get_db_path,
        watch,
    )

    if args.command == "postprocess":
        repo_root = Path(args.repo) if args.repo else find_project_root()
        _handle_data_dir_option(args, repo_root)
        db_path = get_db_path(repo_root)
        store = GraphStore(db_path)
        try:
            from .tools.build import run_postprocess

            result = run_postprocess(
                flows=not getattr(args, "no_flows", False),
                communities=not getattr(args, "no_communities", False),
                fts=not getattr(args, "no_fts", False),
                repo_root=str(repo_root),
            )
            parts = []
            if result.get("flows_detected"):
                parts.append(f"{result['flows_detected']} flows")
            if result.get("communities_detected"):
                parts.append(f"{result['communities_detected']} communities")
            if result.get("fts_indexed"):
                parts.append(f"{result['fts_indexed']} FTS entries")
            print(f"Post-processing: {', '.join(parts) or 'done'}")
        finally:
            store.close()
        return

    if args.command == "embed":
        repo_root = Path(args.repo) if args.repo else find_project_root()
        _handle_data_dir_option(args, repo_root)
        from .tools.docs import embed_graph

        result = embed_graph(
            repo_root=str(repo_root),
            model=args.model,
            provider=args.provider,
        )
        if result.get("status") == "error":
            logging.error(result.get("error", "embed_graph failed"))
            sys.exit(1)
        print(result.get("summary", "Embedding done."))
        return

    if args.command in ("update", "detect-changes"):
        # update and detect-changes require git for diffing
        repo_root = Path(args.repo) if args.repo else find_repo_root()
        if not repo_root:
            logging.error(
                "Not in a git repository. '%s' requires git for diffing.",
                args.command,
            )
            logging.error("Use 'build' for a full parse, or run 'git init' first.")
            sys.exit(1)
    else:
        repo_root = Path(args.repo) if args.repo else find_project_root()

    # Handle --data-dir for commands that support it
    _data_dir_cmds = ("build", "update", "detect-changes", "status", "watch", "visualize", "wiki")
    if args.command in _data_dir_cmds:
        _handle_data_dir_option(args, repo_root)

    db_path = get_db_path(repo_root)
    store = GraphStore(db_path)

    try:
        if args.command == "build":
            pp = (
                "none"
                if getattr(args, "skip_postprocess", False)
                else ("minimal" if getattr(args, "skip_flows", False) else "full")
            )
            from .tools.build import build_or_update_graph

            result = build_or_update_graph(
                full_rebuild=True,
                repo_root=str(repo_root),
                postprocess=pp,
            )
            parsed = result.get("files_parsed", 0)
            nodes = result.get("total_nodes", 0)
            edges = result.get("total_edges", 0)
            print(f"Full build: {parsed} files, {nodes} nodes, {edges} edges (postprocess={pp})")
            if result.get("errors"):
                print(f"Errors: {len(result['errors'])}")

        elif args.command == "update":
            pp = (
                "none"
                if getattr(args, "skip_postprocess", False)
                else ("minimal" if getattr(args, "skip_flows", False) else "full")
            )
            from .tools.build import build_or_update_graph

            result = build_or_update_graph(
                full_rebuild=False,
                repo_root=str(repo_root),
                base=args.base,
                postprocess=pp,
            )
            updated = result.get("files_updated", 0)
            nodes = result.get("total_nodes", 0)
            edges = result.get("total_edges", 0)
            print(
                f"Incremental: {updated} files updated, "
                f"{nodes} nodes, {edges} edges"
                f" (postprocess={pp})"
            )

            # --brief: append a one-line change-impact summary with the same
            # estimated context-savings approximation that detect-changes uses.
            # Same baseline (changed files vs analysis response), so the two
            # commands are directly comparable.
            if getattr(args, "brief", False):
                from .changes import analyze_changes
                from .context_savings import (
                    attach_context_savings,
                    estimate_file_tokens,
                    format_context_savings_panel,
                )
                from .incremental import (
                    get_changed_files,
                    get_staged_and_unstaged,
                )

                changed = get_changed_files(repo_root, args.base)
                if not changed:
                    changed = get_staged_and_unstaged(repo_root)
                if changed:
                    impact = analyze_changes(
                        store,
                        changed,
                        repo_root=str(repo_root),
                        base=args.base,
                    )
                    original_tokens = estimate_file_tokens(repo_root, changed)
                    attach_context_savings(
                        impact,
                        original_tokens=original_tokens,
                    )
                    summary = impact.get("summary", "")
                    if summary:
                        print(summary)
                    verified = None
                    if getattr(args, "verify", False):
                        from .context_savings import verify_with_tiktoken
                        verified = verify_with_tiktoken(
                            repo_root, changed, impact,
                        )
                        if verified is None:
                            print(
                                "Note: --verify requires tiktoken. "
                                "Install with `pip install tiktoken`.",
                            )
                    panel = format_context_savings_panel(
                        impact.get("context_savings"),
                        original_tokens=original_tokens,
                        response=impact,
                        verified=verified,
                    )
                    if panel:
                        print(panel)

        elif args.command == "status":
            stats = store.get_stats()
            print(f"Nodes: {stats.total_nodes}")
            print(f"Edges: {stats.total_edges}")
            print(f"Files: {stats.files_count}")
            print(f"Languages: {', '.join(stats.languages)}")
            print(f"Last updated: {stats.last_updated or 'never'}")
            # Show branch info and warn if stale
            stored_branch = store.get_metadata("git_branch")
            stored_sha = store.get_metadata("git_head_sha")
            if stored_branch:
                print(f"Built on branch: {stored_branch}")
            if stored_sha:
                print(f"Built at commit: {stored_sha[:12]}")
            from .incremental import _git_branch_info, detect_vcs
            vcs = detect_vcs(repo_root)
            if vcs == "git":
                current_branch, current_sha = _git_branch_info(repo_root)
                if stored_branch and current_branch and stored_branch != current_branch:
                    print(
                        f"WARNING: Graph was built on '{stored_branch}' "
                        f"but you are now on '{current_branch}'. "
                        f"Run 'code-review-graph build' to rebuild."
                    )
            elif vcs == "svn":
                stored_rev = store.get_metadata("svn_revision")
                stored_svn_branch = store.get_metadata("svn_branch")
                if stored_svn_branch:
                    print(f"SVN branch: {stored_svn_branch}")
                if stored_rev:
                    print(f"SVN revision at build: {stored_rev}")

        elif args.command == "watch":
            from .postprocessing import run_post_processing

            try:
                watch(repo_root, store, on_files_updated=run_post_processing)
            except RuntimeError as exc:
                print(f"Error: {exc}", file=sys.stderr)
                sys.exit(1)

        elif args.command == "visualize":
            from .incremental import get_data_dir

            data_dir = get_data_dir(repo_root)
            fmt = getattr(args, "format", "html") or "html"

            if fmt == "graphml":
                from .exports import export_graphml

                out = data_dir / "graph.graphml"
                export_graphml(store, out)
                print(f"GraphML exported: {out}")
            elif fmt == "cypher":
                from .exports import export_neo4j_cypher

                out = data_dir / "graph.cypher"
                export_neo4j_cypher(store, out)
                print(f"Neo4j Cypher exported: {out}")
            elif fmt == "obsidian":
                from .exports import export_obsidian_vault

                out = data_dir / "obsidian"
                export_obsidian_vault(store, out)
                print(f"Obsidian vault exported: {out}")
            elif fmt == "svg":
                from .exports import export_svg

                out = data_dir / "graph.svg"
                export_svg(store, out)
                print(f"SVG exported: {out}")
            else:
                from .visualization import generate_html

                html_path = data_dir / "graph.html"
                vis_mode = getattr(args, "mode", "auto") or "auto"
                generate_html(store, html_path, mode=vis_mode)
                print(f"Visualization ({vis_mode}): {html_path}")
                if getattr(args, "serve", False):
                    import functools
                    import http.server

                    serve_dir = html_path.parent
                    port = 8765
                    http_handler = functools.partial(
                        http.server.SimpleHTTPRequestHandler,
                        directory=str(serve_dir),
                    )
                    print(f"Serving at http://localhost:{port}/graph.html")
                    print("Press Ctrl+C to stop.")
                    with http.server.HTTPServer(("localhost", port), http_handler) as httpd:
                        try:
                            httpd.serve_forever()
                        except KeyboardInterrupt:
                            print("\nServer stopped.")
                else:
                    print("Open in browser to explore.")

        elif args.command == "wiki":
            from .incremental import get_data_dir
            from .wiki import generate_wiki

            wiki_dir = get_data_dir(repo_root) / "wiki"
            result = generate_wiki(store, wiki_dir, force=args.force)
            total = result["pages_generated"] + result["pages_updated"] + result["pages_unchanged"]
            print(
                f"Wiki: {result['pages_generated']} new, "
                f"{result['pages_updated']} updated, "
                f"{result['pages_unchanged']} unchanged "
                f"({total} total pages)"
            )
            print(f"Output: {wiki_dir}")

        elif args.command == "detect-changes":
            from .changes import analyze_changes
            from .context_savings import (
                attach_context_savings,
                estimate_file_tokens,
            )
            from .incremental import get_changed_files, get_staged_and_unstaged

            base = args.base
            changed = get_changed_files(repo_root, base)
            if not changed:
                changed = get_staged_and_unstaged(repo_root)

            if not changed:
                print("No changes detected.")
            else:
                result = analyze_changes(
                    store,
                    changed,
                    repo_root=str(repo_root),
                    base=base,
                )
                original_tokens = estimate_file_tokens(repo_root, changed)
                attach_context_savings(
                    result,
                    original_tokens=original_tokens,
                )
                if args.brief:
                    from .context_savings import (
                        format_context_savings_panel,
                        verify_with_tiktoken,
                    )
                    print(result.get("summary", "No summary available."))
                    verified = None
                    if getattr(args, "verify", False):
                        verified = verify_with_tiktoken(repo_root, changed, result)
                        if verified is None:
                            print(
                                "Note: --verify requires tiktoken. "
                                "Install with `pip install tiktoken`.",
                            )
                    panel = format_context_savings_panel(
                        result.get("context_savings"),
                        original_tokens=original_tokens,
                        response=result,
                        verified=verified,
                    )
                    if panel:
                        print(panel)
                else:
                    print(json.dumps(result, indent=2, default=str))

    finally:
        store.close()
