"""
Command-line interface for Flyto Indexer.

Usage:
    flyto-index init [path]
    flyto-index scan <path> [--full]
    flyto-index status [path]
    flyto-index impact <symbol_id> --path <project_path>
    flyto-index context --path <project_path> [--query <query>]
    flyto-index outline <path>
    flyto-index brief [path]
    flyto-index describe <file_path> [--summary "..."] [--path <project_path>]
    flyto-index install-hook [path] [--remove]
    flyto-index demo [path]
    flyto-index check [path] [--threshold high|medium|low] [--json] [--base <ref>]
"""

import argparse
import json
import os
import subprocess
import sys
import time
from pathlib import Path


def _ensure_gitignore(directory: Path):
    """Create .gitignore with '*' in a generated directory if it doesn't exist."""
    gi = directory / ".gitignore"
    if not gi.exists():
        gi.write_text("# Auto-generated by flyto-indexer — keep IDE and git from indexing this directory\n*\n")


def main():
    parser = argparse.ArgumentParser(
        prog="flyto-index",
        description="Flyto Indexer - Code indexing, search, and impact analysis for AI-assisted development",
        epilog=(
            "Examples:\n"
            "  flyto-index init .                  Initialize indexing for current project\n"
            "  flyto-index scan . --full            Full re-index of current project\n"
            "  flyto-index status                   Check index freshness\n"
            "  flyto-index impact useAuth --path .  See what depends on useAuth\n"
            "  flyto-index tools                    List all commands as JSON (for AI integration)\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # init
    init_parser = subparsers.add_parser(
        "init",
        help="Initialize .flyto/ in a project",
        description="Create the .flyto/ directory structure for a project. This is required before scanning.",
    )
    init_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")
    init_parser.add_argument("--name", help="Project name (default: directory name)")
    init_parser.add_argument("--no-gitignore", action="store_true", help="Do not add .flyto/ to .gitignore")
    init_parser.add_argument("--index", action="store_true", help="Run indexer immediately after init")

    # scan
    scan_parser = subparsers.add_parser(
        "scan",
        help="Scan project and build/update code index",
        description="Parse all source files, extract symbols (functions, classes, components), build dependency graph, and detect dead code.",
    )
    scan_parser.add_argument("path", help="Project root path")
    scan_parser.add_argument("--full", action="store_true", help="Full rebuild instead of incremental update")
    scan_parser.add_argument("--name", help="Project name (default: directory name)")
    scan_parser.add_argument("--output", help="Index output directory (default: .flyto/)")

    # impact
    impact_parser = subparsers.add_parser(
        "impact",
        help="Analyze what would be affected by changing a symbol",
        description="Show all code that depends on a given symbol. Use before modifying shared functions or components.",
    )
    impact_parser.add_argument("symbol_id", help="Symbol ID or name (e.g., 'useAuth' or 'project:path:type:name')")
    impact_parser.add_argument("--path", required=True, help="Project root path")
    impact_parser.add_argument("--depth", type=int, default=3, help="Max analysis depth (default: 3)")

    # context
    context_parser = subparsers.add_parser(
        "context",
        help="Get AI-ready context for a project",
        description="Extract structured context (symbols, summaries, dependencies) suitable for feeding to an LLM.",
    )
    context_parser.add_argument("--path", required=True, help="Project root path")
    context_parser.add_argument("--query", help="Natural language query to focus context on")
    context_parser.add_argument("--files", nargs="+", help="Specific files to include (L1 detail)")
    context_parser.add_argument("--symbols", nargs="+", help="Specific symbols to include (L2 detail)")
    context_parser.add_argument("--level", choices=["l0", "l1", "l2", "auto"], default="auto", help="Detail level: l0=outline, l1=file, l2=symbol, auto=adaptive (default: auto)")

    # status
    status_parser = subparsers.add_parser(
        "status",
        help="Show index status (file count, symbol count, staleness)",
        description="Display .flyto/ index statistics and check if re-indexing is needed.",
    )
    status_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")
    status_parser.add_argument("--json", action="store_true", dest="as_json", help="Output as JSON instead of human-readable text")

    # brief
    brief_parser = subparsers.add_parser(
        "brief",
        help="Generate a brief (<500 token) project summary",
        description="Create a concise project overview from .flyto/ data, suitable for LLM system prompts.",
    )
    brief_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")

    # describe
    describe_parser = subparsers.add_parser(
        "describe",
        help="Read or write a one-liner semantic description for a file",
        description="Manage file descriptions stored in .flyto/descriptions.jsonl. Omit --summary to read, include it to write.",
    )
    describe_parser.add_argument("file_path", help="File path relative to project root (e.g., src/api/auth.py)")
    describe_parser.add_argument("--summary", help="One-liner description to write (omit to read existing)")
    describe_parser.add_argument("--path", default=".", help="Project root path (default: current directory)")
    describe_parser.add_argument("--source", default="ai", help="Description source tag (default: ai)")

    # outline
    outline_parser = subparsers.add_parser(
        "outline",
        help="Generate project outline (L0 structure overview)",
        description="Create a high-level map of project structure, categories, and key entry points.",
    )
    outline_parser.add_argument("path", help="Project root path")
    outline_parser.add_argument("--name", help="Project name (default: directory name)")

    # tools
    tools_parser = subparsers.add_parser(
        "tools",
        help="List all commands as structured JSON (for AI/LLM integration)",
        description="Output machine-readable JSON describing all available commands, their arguments, expected outputs, side effects, and examples. Feed this to an LLM so it knows how to use flyto-index.",
    )
    tools_parser.add_argument("--json", action="store_true", dest="as_json", default=True, help="Output as JSON (default)")
    tools_parser.add_argument("--compact", action="store_true", help="Compact output: names and one-liner summaries only")

    # install-hook
    hook_parser = subparsers.add_parser(
        "install-hook",
        help="Install git post-commit hook for auto-reindexing",
        description="Install a git post-commit hook that automatically runs incremental scan after each commit.",
    )
    hook_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")
    hook_parser.add_argument("--remove", action="store_true", help="Remove the flyto hook instead of installing it")

    # demo
    demo_parser = subparsers.add_parser(
        "demo",
        help="Quick 30-second value demo (scan + impact analysis)",
        description="Scan the project and demonstrate impact analysis on the most-referenced symbol. No MCP required.",
    )
    demo_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")

    # setup
    setup_parser = subparsers.add_parser(
        "setup",
        help="One command to set up everything: scan + CLAUDE.md + MCP config",
        description="Complete setup for AI-assisted development. Scans the project, writes CLAUDE.md instructions, and configures Claude Code MCP settings. Run this once per project.",
    )
    setup_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")
    setup_parser.add_argument("--remove", action="store_true", help="Remove flyto-indexer from CLAUDE.md and MCP settings")

    # setup-claude (kept for backward compat)
    setup_claude_parser = subparsers.add_parser(
        "setup-claude",
        help="Add flyto-indexer usage instructions to CLAUDE.md",
        description="Append task contract and tool usage instructions to CLAUDE.md so AI assistants know to use flyto-indexer.",
    )
    setup_claude_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")
    setup_claude_parser.add_argument("--remove", action="store_true", help="Remove flyto-indexer section from CLAUDE.md")

    # check
    check_parser = subparsers.add_parser(
        "check",
        help="CI-friendly impact check — exits non-zero when changes are risky",
        description="Detect changed files, analyze impact, and exit non-zero if risk exceeds threshold. Designed for CI pipelines.",
    )
    check_parser.add_argument("path", nargs="?", default=".", help="Project root path (default: current directory)")
    check_parser.add_argument("--threshold", choices=["high", "medium", "low"], default="high", help="Fail when risk >= this level (default: high)")
    check_parser.add_argument("--json", action="store_true", dest="as_json", help="Output as structured JSON")
    check_parser.add_argument("--base", help="Git ref to compare against (default: detect from index staleness)")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        return

    try:
        if args.command == "init":
            result = cmd_init(args)
        elif args.command == "scan":
            result = cmd_scan(args)
        elif args.command == "status":
            result = cmd_status(args)
        elif args.command == "impact":
            result = cmd_impact(args)
        elif args.command == "context":
            result = cmd_context(args)
        elif args.command == "brief":
            result = cmd_brief(args)
        elif args.command == "describe":
            result = cmd_describe(args)
        elif args.command == "outline":
            result = cmd_outline(args)
        elif args.command == "tools":
            result = cmd_tools(args)
        elif args.command == "install-hook":
            result = cmd_install_hook(args)
        elif args.command == "demo":
            result = cmd_demo(args)
        elif args.command == "setup":
            result = cmd_setup(args)
        elif args.command == "setup-claude":
            result = cmd_setup_claude(args)
        elif args.command == "check":
            result = cmd_check(args)
        else:
            parser.print_help()
            return

        # Output result
        if result is None:
            pass  # Command handled its own output
        elif isinstance(result, str):
            print(result)
        else:
            print(json.dumps(result, indent=2, ensure_ascii=False))

    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


def cmd_init(args):
    """Initialize .flyto/ in a project directory."""
    from datetime import datetime, timezone

    project_path = Path(args.path).resolve()
    project_name = args.name or project_path.name

    if not project_path.exists():
        print(f"Path does not exist: {project_path}", file=sys.stderr)
        sys.exit(1)

    flyto_dir = project_path / ".flyto"
    nav_dir = flyto_dir / "nav"
    index_dir = flyto_dir / "index"

    if flyto_dir.exists():
        print(f".flyto/ already exists at {flyto_dir}")
        print("Use 'flyto-index scan' to update the index.")
        return {"ok": True, "message": "already exists", "path": str(flyto_dir)}

    # Create minimal structure
    nav_dir.mkdir(parents=True, exist_ok=True)
    index_dir.mkdir(parents=True, exist_ok=True)
    _ensure_gitignore(flyto_dir)

    # Auto-exclude from IDE file watchers (WebStorm, VSCode, etc.)
    from .engine import _ensure_ide_exclude
    _ensure_ide_exclude(project_path)

    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    # Write flyto.json
    flyto_json = {
        "schemaVersion": 1,
        "generatedAt": now,
        "project": {"name": project_name, "root": "."},
        "generator": {"name": "flyto-indexer", "version": "0.2.0"},
        "paths": {
            "map": "nav/map.json",
            "descriptions": "descriptions.jsonl",
            "indexSummary": "index/summary.json",
        },
    }
    (flyto_dir / "flyto.json").write_text(
        json.dumps(flyto_json, indent=2, ensure_ascii=False) + "\n",
        encoding="utf-8",
    )

    # Write empty nav/map.json
    map_json = {
        "schemaVersion": 1,
        "generatedAt": now,
        "categories": [],
        "hotspots": [],
    }
    (nav_dir / "map.json").write_text(
        json.dumps(map_json, indent=2, ensure_ascii=False) + "\n",
        encoding="utf-8",
    )

    # Write empty descriptions.jsonl
    desc_path = flyto_dir / "descriptions.jsonl"
    if not desc_path.exists():
        desc_path.write_text("", encoding="utf-8")

    # Write empty index/summary.json
    summary_json = {
        "schemaVersion": 1,
        "generatedAt": now,
        "counts": {"files": 0, "folders": 0, "symbols": 0, "languages": {}},
        "stalenessHint": {"recommendedReindexAfterHours": 24},
    }
    (index_dir / "summary.json").write_text(
        json.dumps(summary_json, indent=2, ensure_ascii=False) + "\n",
        encoding="utf-8",
    )

    print(f"Initialized .flyto/ at {flyto_dir}")

    # Add to .gitignore
    if not args.no_gitignore:
        gitignore_path = project_path / ".gitignore"
        entry = ".flyto/"
        already_ignored = False
        if gitignore_path.exists():
            content = gitignore_path.read_text()
            already_ignored = entry in content.splitlines()
        if not already_ignored:
            with open(gitignore_path, "a") as f:
                if gitignore_path.exists() and not gitignore_path.read_text().endswith("\n"):
                    f.write("\n")
                f.write(f"\n# Flyto index (generated)\n{entry}\n")
            print(f"Added '{entry}' to .gitignore")

    # Optionally run indexer
    if args.index:
        print("\nRunning indexer...")
        args_scan = argparse.Namespace(
            path=str(project_path), full=False, name=project_name, output=None
        )
        return cmd_scan(args_scan)

    return {"ok": True, "path": str(flyto_dir)}


def cmd_status(args):
    """Show .flyto/ status for a project."""
    project_path = Path(args.path).resolve()
    flyto_dir = project_path / ".flyto"

    if not flyto_dir.exists():
        print(f"No .flyto/ found at {project_path}")
        print("Run 'flyto-index init' to initialize.")
        return {"ok": False, "error": "no .flyto/ found"}

    # Read flyto.json
    flyto_json = {}
    flyto_path = flyto_dir / "flyto.json"
    if flyto_path.exists():
        flyto_json = json.loads(flyto_path.read_text(encoding="utf-8"))

    # Read summary.json
    summary = {}
    summary_path = flyto_dir / "index" / "summary.json"
    if summary_path.exists():
        summary = json.loads(summary_path.read_text(encoding="utf-8"))

    # Count tags
    tags_path = flyto_dir / "tags" / "symbol_tags.jsonl"
    tag_counts = {}
    tag_total = 0
    if tags_path.exists():
        for line in tags_path.read_text(encoding="utf-8").strip().split("\n"):
            if not line.strip():
                continue
            try:
                tag = json.loads(line)
                kind = tag.get("kind", "unknown")
                tag_counts[kind] = tag_counts.get(kind, 0) + 1
                tag_total += 1
            except json.JSONDecodeError:
                pass

    # Count descriptions
    desc_path = flyto_dir / "descriptions.jsonl"
    desc_count = 0
    if desc_path.exists():
        content = desc_path.read_text(encoding="utf-8").strip()
        if content:
            desc_count = len(content.split("\n"))

    # Build result
    counts = summary.get("counts", {})
    tag_stats = summary.get("tags", {})
    desc_stats = summary.get("descriptions", {})
    result = {
        "ok": True,
        "project": flyto_json.get("project", {}).get("name", "unknown"),
        "schemaVersion": flyto_json.get("schemaVersion", 0),
        "generatedAt": flyto_json.get("generatedAt", ""),
        "generator": flyto_json.get("generator", {}).get("version", ""),
        "counts": counts,
        "tags": tag_stats if tag_stats else tag_counts,
        "tags_total": tag_total,
        "descriptions": desc_stats if desc_stats else {"total": desc_count},
    }

    if hasattr(args, "as_json") and args.as_json:
        return result

    # Human-readable output
    print(f"Flyto Status: {result['project']}")
    print(f"  Schema:     v{result['schemaVersion']}")
    print(f"  Generated:  {result['generatedAt']}")
    print(f"  Generator:  flyto-indexer {result['generator']}")
    print()
    print(f"  Files:      {counts.get('files', 0)}")
    print(f"  Symbols:    {counts.get('symbols', 0)}")
    print(f"  Languages:  {counts.get('languages', {})}")
    print()
    if tag_stats:
        print("  Tags:")
        print(f"    Dead code:      {tag_stats.get('dead_code', 0)} symbols ({tag_stats.get('dead_code_lines', 0)} lines)")
        print(f"    TDD covered:    {tag_stats.get('tdd_covered', 0)} / {tag_stats.get('tdd_testable', 0)} testable")
        print(f"    TDD uncovered:  {tag_stats.get('tdd_uncovered', 0)}")
    elif tag_counts:
        print(f"  Tags: {tag_counts}")
    else:
        print("  Tags: (none)")
    print()
    if desc_stats:
        desc_total = desc_stats.get("total", 0)
        desc_fresh = desc_stats.get("fresh", 0)
        hs_total = desc_stats.get("hotspot_total", 0)
        hs_covered = desc_stats.get("hotspot_covered", 0)
        print(f"  Descriptions: {desc_fresh}/{desc_total} files covered")
        if hs_total > 0:
            print(f"    Hotspots:   {hs_covered}/{hs_total} covered")
    else:
        print(f"  Descriptions: {desc_count} entries")

    return result


def cmd_scan(args):
    """Execute scan command"""
    from .engine import IndexEngine

    project_path = Path(args.path).resolve()
    project_name = args.name or project_path.name
    index_dir = Path(args.output) if args.output else None

    engine = IndexEngine(project_name, project_path, index_dir)
    result = engine.scan(incremental=not args.full)

    return result


def cmd_impact(args):
    """Execute impact command"""
    from .engine import IndexEngine

    project_path = Path(args.path).resolve()
    project_name = project_path.name

    engine = IndexEngine(project_name, project_path)
    result = engine.impact(args.symbol_id, args.depth)

    return result


def cmd_context(args):
    """Execute context command"""
    from .engine import IndexEngine

    project_path = Path(args.path).resolve()
    project_name = project_path.name

    engine = IndexEngine(project_name, project_path)
    result = engine.context(
        query=args.query,
        paths=args.files,
        symbols=args.symbols,
        level=args.level,
    )

    return result


def cmd_outline(args):
    """Execute outline command"""
    from .engine import IndexEngine

    project_path = Path(args.path).resolve()
    project_name = args.name or project_path.name

    engine = IndexEngine(project_name, project_path)
    return engine.outline()


def cmd_brief(args):
    """Generate or display .flyto/brief.md."""
    from .flyto_output import generate_brief_from_flyto

    project_path = Path(args.path).resolve()
    content = generate_brief_from_flyto(project_path)
    print(content)


def cmd_describe(args):
    """Read or write file descriptions in .flyto/descriptions.jsonl."""
    import hashlib
    from datetime import datetime, timezone

    project_path = Path(args.path).resolve()
    flyto_dir = project_path / ".flyto"
    desc_path = flyto_dir / "descriptions.jsonl"

    if not flyto_dir.exists():
        print(f"No .flyto/ found at {project_path}", file=sys.stderr)
        print("Run 'flyto-index init' to initialize.", file=sys.stderr)
        sys.exit(1)

    file_path = args.file_path

    if args.summary:
        # Write mode: append/update description
        # Compute file hash if the file exists
        full_file_path = project_path / file_path
        file_hash = ""
        if full_file_path.exists():
            content = full_file_path.read_bytes()
            file_hash = hashlib.sha256(content).hexdigest()[:16]

        now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        entry = {
            "path": file_path,
            "hash": file_hash,
            "one_liner": args.summary,
            "source": args.source,
            "updatedAt": now,
        }
        line = json.dumps(entry, ensure_ascii=False)

        # Append to descriptions.jsonl
        with open(desc_path, "a", encoding="utf-8") as f:
            if desc_path.exists() and desc_path.stat().st_size > 0:
                # Check if file ends with newline
                with open(desc_path, "rb") as check:
                    check.seek(-1, 2)
                    if check.read(1) != b"\n":
                        f.write("\n")
            f.write(line + "\n")

        print(f"Updated: {file_path}")
        print(f"  {args.summary}")

    else:
        # Read mode: find latest description for this file
        if not desc_path.exists():
            print("No descriptions found. Run 'flyto-index scan' first.", file=sys.stderr)
            sys.exit(1)

        latest = None
        for line in desc_path.read_text(encoding="utf-8").strip().split("\n"):
            if not line.strip():
                continue
            try:
                entry = json.loads(line)
                if entry.get("path") == file_path:
                    latest = entry
            except json.JSONDecodeError:
                pass

        if latest:
            # Check staleness
            full_file_path = project_path / file_path
            stale = False
            if full_file_path.exists() and latest.get("hash"):
                import hashlib
                current_hash = hashlib.sha256(full_file_path.read_bytes()).hexdigest()[:16]
                if current_hash != latest["hash"]:
                    stale = True

            print(f"File: {file_path}")
            print(f"  {latest.get('one_liner', '(no description)')}")
            if stale:
                print("  [STALE] File has changed since this description was written.")
            print(f"  Source: {latest.get('source', 'unknown')}")
            print(f"  Updated: {latest.get('updatedAt', 'unknown')}")
        else:
            print(f"No description found for: {file_path}")


def cmd_tools(args):
    """Output structured JSON describing all available CLI commands and their arguments."""
    from datetime import datetime, timezone

    commands = [
        {
            "name": "init",
            "summary": "Initialize .flyto/ directory in a project for indexing",
            "args": [
                {"name": "path", "type": "string", "required": False, "default": ".", "description": "Project root path"},
                {"name": "--name", "type": "string", "required": False, "description": "Project name (default: directory name)"},
                {"name": "--no-gitignore", "type": "boolean", "required": False, "default": False, "description": "Do not add .flyto/ to .gitignore"},
                {"name": "--index", "type": "boolean", "required": False, "default": False, "description": "Run indexing immediately after init"},
            ],
            "outputs": [".flyto/flyto.json", ".flyto/nav/map.json", ".flyto/descriptions.jsonl", ".flyto/index/summary.json"],
            "side_effects": ["creates .flyto/ directory", "may modify .gitignore"],
            "examples": ["flyto-index init", "flyto-index init ./my-project --name myapp --index"],
            "exit_codes": {"0": "success", "1": "error"},
        },
        {
            "name": "scan",
            "summary": "Scan a project and build/update the code index",
            "args": [
                {"name": "path", "type": "string", "required": True, "description": "Project root path"},
                {"name": "--full", "type": "boolean", "required": False, "default": False, "description": "Full rebuild (not incremental)"},
                {"name": "--name", "type": "string", "required": False, "description": "Project name (default: directory name)"},
                {"name": "--output", "type": "string", "required": False, "description": "Index output directory"},
            ],
            "outputs": [".flyto/index/", ".flyto/tags/", ".flyto/descriptions.jsonl"],
            "side_effects": ["writes index files to .flyto/"],
            "examples": ["flyto-index scan .", "flyto-index scan ./my-project --full --name myapp"],
            "exit_codes": {"0": "success", "1": "error"},
        },
        {
            "name": "status",
            "summary": "Show .flyto/ index status: file count, symbol count, staleness",
            "args": [
                {"name": "path", "type": "string", "required": False, "default": ".", "description": "Project root path"},
                {"name": "--json", "type": "boolean", "required": False, "default": False, "description": "Output as JSON"},
            ],
            "outputs": [],
            "side_effects": [],
            "examples": ["flyto-index status", "flyto-index status ./my-project --json"],
            "exit_codes": {"0": "success", "1": "no .flyto/ found"},
        },
        {
            "name": "impact",
            "summary": "Analyze what would be affected by changing a symbol",
            "args": [
                {"name": "symbol_id", "type": "string", "required": True, "description": "Symbol ID (format: project:path:type:name)"},
                {"name": "--path", "type": "string", "required": True, "description": "Project root path"},
                {"name": "--depth", "type": "integer", "required": False, "default": 3, "description": "Max analysis depth"},
            ],
            "outputs": [],
            "side_effects": [],
            "examples": ["flyto-index impact useAuth --path ."],
            "exit_codes": {"0": "success", "1": "symbol not found"},
        },
        {
            "name": "context",
            "summary": "Get AI-ready context for a project (symbols, files, summaries)",
            "args": [
                {"name": "--path", "type": "string", "required": True, "description": "Project root path"},
                {"name": "--query", "type": "string", "required": False, "description": "Natural language query to focus context"},
                {"name": "--files", "type": "string[]", "required": False, "description": "Specific files to include"},
                {"name": "--symbols", "type": "string[]", "required": False, "description": "Specific symbols to include"},
                {"name": "--level", "type": "string", "required": False, "default": "auto", "description": "Detail level: l0 (outline), l1 (file), l2 (symbol), auto"},
            ],
            "outputs": [],
            "side_effects": [],
            "examples": ["flyto-index context --path . --query 'authentication flow'"],
            "exit_codes": {"0": "success", "1": "error"},
        },
        {
            "name": "outline",
            "summary": "Generate a project outline (L0 overview of structure and categories)",
            "args": [
                {"name": "path", "type": "string", "required": True, "description": "Project root path"},
                {"name": "--name", "type": "string", "required": False, "description": "Project name"},
            ],
            "outputs": [],
            "side_effects": [],
            "examples": ["flyto-index outline ."],
            "exit_codes": {"0": "success", "1": "error"},
        },
        {
            "name": "brief",
            "summary": "Generate a brief (<500 token) project summary from .flyto/ data",
            "args": [
                {"name": "path", "type": "string", "required": False, "default": ".", "description": "Project root path"},
            ],
            "outputs": [],
            "side_effects": [],
            "examples": ["flyto-index brief", "flyto-index brief ./my-project"],
            "exit_codes": {"0": "success", "1": "no .flyto/ found"},
        },
        {
            "name": "describe",
            "summary": "Read or write a one-liner semantic description for a file",
            "args": [
                {"name": "file_path", "type": "string", "required": True, "description": "File path relative to project root"},
                {"name": "--summary", "type": "string", "required": False, "description": "Description to write (omit to read existing)"},
                {"name": "--path", "type": "string", "required": False, "default": ".", "description": "Project root path"},
                {"name": "--source", "type": "string", "required": False, "default": "ai", "description": "Description source tag"},
            ],
            "outputs": [],
            "side_effects": ["appends to .flyto/descriptions.jsonl (write mode only)"],
            "examples": [
                "flyto-index describe src/api/auth.py",
                "flyto-index describe src/api/auth.py --summary 'User auth: login, register, JWT'",
            ],
            "exit_codes": {"0": "success", "1": "error"},
        },
        {
            "name": "tools",
            "summary": "List all available commands with arguments, examples, and side effects (this output)",
            "args": [
                {"name": "--json", "type": "boolean", "required": False, "default": True, "description": "Output as JSON (default)"},
                {"name": "--compact", "type": "boolean", "required": False, "default": False, "description": "Compact output (names and summaries only)"},
            ],
            "outputs": [],
            "side_effects": [],
            "examples": ["flyto-index tools", "flyto-index tools --compact"],
            "exit_codes": {"0": "success"},
        },
    ]

    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    if hasattr(args, "compact") and args.compact:
        return {
            "binary": "flyto-index",
            "generatedAt": now,
            "commands": [{"name": c["name"], "summary": c["summary"]} for c in commands],
        }

    return {
        "binary": "flyto-index",
        "version": "0.1.0",
        "generatedAt": now,
        "usage": "flyto-index <command> [args]",
        "commands": commands,
    }


HOOK_MARKER_BEGIN = "# --- flyto-indexer begin ---"
HOOK_MARKER_END = "# --- flyto-indexer end ---"
HOOK_CONTENT = """\
# --- flyto-indexer begin ---
flyto-index scan . 2>/dev/null &
# --- flyto-indexer end ---
"""


def cmd_install_hook(args):
    """Install or remove git post-commit hook for auto-reindexing."""
    project_path = Path(args.path).resolve()
    git_dir = project_path / ".git"

    if not git_dir.is_dir():
        print(f"Not a git repository: {project_path}", file=sys.stderr)
        sys.exit(1)

    hooks_dir = git_dir / "hooks"
    hooks_dir.mkdir(exist_ok=True)
    hook_file = hooks_dir / "post-commit"

    if args.remove:
        # Remove flyto lines between markers
        if not hook_file.exists():
            print("No post-commit hook found. Nothing to remove.")
            return None

        content = hook_file.read_text(encoding="utf-8")
        if HOOK_MARKER_BEGIN not in content:
            print("No flyto hook found in post-commit. Nothing to remove.")
            return None

        # Strip lines between markers (inclusive)
        lines = content.splitlines(keepends=True)
        new_lines = []
        inside_marker = False
        for line in lines:
            if HOOK_MARKER_BEGIN in line:
                inside_marker = True
                continue
            if HOOK_MARKER_END in line:
                inside_marker = False
                continue
            if not inside_marker:
                new_lines.append(line)

        new_content = "".join(new_lines).strip()
        # If only a shebang line remains, treat as empty
        if new_content and new_content.strip().replace("#!/bin/sh", "").strip():
            hook_file.write_text(new_content + "\n", encoding="utf-8")
        else:
            hook_file.unlink()

        print(f"Removed flyto hook from {hook_file}")
        return None

    # Install mode
    if hook_file.exists():
        content = hook_file.read_text(encoding="utf-8")
        if HOOK_MARKER_BEGIN in content:
            print("Flyto hook already installed. Skipping.")
            return None
        # Append to existing hook
        if not content.endswith("\n"):
            content += "\n"
        content += "\n" + HOOK_CONTENT
        hook_file.write_text(content, encoding="utf-8")
    else:
        hook_file.write_text("#!/bin/sh\n\n" + HOOK_CONTENT, encoding="utf-8")

    os.chmod(hook_file, 0o755)
    print(f"Installed flyto post-commit hook at {hook_file}")
    return None


def cmd_demo(args):
    """Quick 30-second value demo: scan + impact analysis."""
    from .engine import IndexEngine

    project_path = Path(args.path).resolve()
    project_name = project_path.name

    print(f"Scanning {project_path.name}...", end=" ", flush=True)
    t0 = time.monotonic()

    engine = IndexEngine(project_name, project_path)
    engine.scan(incremental=True)

    elapsed = time.monotonic() - t0
    symbol_count = len(engine.index.symbols)

    if symbol_count == 0:
        print("done")
        print()
        print("No symbols found. Make sure the project has supported source files")
        print("(.py, .ts, .tsx, .js, .jsx, .vue, .go, .rs, .java).")
        return None

    print(f"done ({symbol_count} symbols in {elapsed:.1f}s)")
    print()

    # Find symbol with highest reference_count
    best_symbol = max(
        engine.index.symbols.values(),
        key=lambda s: s.reference_count,
    )

    # Run impact analysis
    impact = engine.impact(best_symbol.id)
    chain = impact.get("impact_chain", [])

    # Collect affected files and total call sites
    total_affected = 0
    affected_files = set()
    for level in chain:
        for entry in level.get("affected", []):
            total_affected += 1
            affected_files.add(entry["path"])

    print(f"Example: What if you change `{best_symbol.name}`?")
    if total_affected > 0:
        file_word = "file" if len(affected_files) == 1 else "files"
        site_word = "call site" if total_affected == 1 else "call sites"
        print(f"  → {total_affected} {site_word} in {len(affected_files)} {file_word}")

        # Determine risk
        if total_affected >= 20:
            risk = "HIGH"
        elif total_affected >= 3:
            risk = "MEDIUM"
        else:
            risk = "LOW"
        print(f"  → Risk: {risk}")

        # Show affected files (max 5)
        shown = sorted(affected_files)[:5]
        print(f"  → Affected: {', '.join(shown)}")
        if len(affected_files) > 5:
            print(f"    ... and {len(affected_files) - 5} more")
    else:
        print("  → No other code depends on this symbol")

    print()
    print(f"Try: flyto-index impact {best_symbol.name} --path .")
    return None


CLAUDE_MARKER_BEGIN = "<!-- flyto-indexer begin -->"
CLAUDE_MARKER_END = "<!-- flyto-indexer end -->"
CLAUDE_SECTION = """\
<!-- flyto-indexer begin -->
## Code Intelligence (flyto-indexer)

This project is indexed by [flyto-indexer](https://pypi.org/project/flyto-indexer/). Use its MCP tools for code changes.

### ALWAYS use flyto-indexer tools when:
- **Auditing or reviewing** a project → `code_health_score`, `security_scan`, `find_dead_code`, `find_complex_functions`
- **Understanding code** → `search_code`, `list_projects`, `list_apis`, `dependency_graph`
- **Before modifying code** → `analyze_task` (call FIRST), then follow the `execution_plan`
- **Checking impact** → `impact_analysis`, `find_references`, `edit_impact_preview`, `cross_project_impact`
- **During modifications** → `task_gate_check` at each phase gate before proceeding

### Workflow for code changes
1. `analyze_task` — get risk dimensions, constraints, and execution plan
2. Follow `execution_plan` steps in order — each step has tool name and pre-filled args
3. `task_gate_check` at gate steps before proceeding to next phase
4. Respect `constraints.max_files_per_step`
<!-- flyto-indexer end -->
"""


CLAUDE_SETTINGS_PATH = Path.home() / ".claude" / "settings.json"
MCP_SERVER_KEY = "flyto-indexer"


def _configure_mcp_settings(remove=False):
    """Add or remove flyto-indexer from ~/.claude/settings.json."""
    settings_path = CLAUDE_SETTINGS_PATH

    if remove:
        if not settings_path.exists():
            return
        settings = json.loads(settings_path.read_text(encoding="utf-8"))
        servers = settings.get("mcpServers", {})
        if MCP_SERVER_KEY not in servers:
            return
        del servers[MCP_SERVER_KEY]
        settings_path.write_text(
            json.dumps(settings, indent=2, ensure_ascii=False) + "\n",
            encoding="utf-8",
        )
        print(f"  Removed {MCP_SERVER_KEY} from {settings_path}")
        return

    # Find python3 path
    python_path = sys.executable

    mcp_entry = {
        "command": python_path,
        "args": ["-m", "flyto_indexer.mcp_server"],
    }

    if settings_path.exists():
        settings = json.loads(settings_path.read_text(encoding="utf-8"))
    else:
        settings_path.parent.mkdir(parents=True, exist_ok=True)
        settings = {}

    servers = settings.setdefault("mcpServers", {})
    if MCP_SERVER_KEY in servers:
        existing = servers[MCP_SERVER_KEY]
        if existing.get("args") == mcp_entry["args"]:
            print(f"  MCP server already configured in {settings_path}")
            return
    servers[MCP_SERVER_KEY] = mcp_entry
    settings_path.write_text(
        json.dumps(settings, indent=2, ensure_ascii=False) + "\n",
        encoding="utf-8",
    )
    print(f"  MCP server configured in {settings_path}")


def cmd_setup(args):
    """One command setup: scan + CLAUDE.md + MCP config."""
    project_path = Path(args.path).resolve()

    if args.remove:
        print("Removing flyto-indexer setup...")
        # Remove CLAUDE.md section
        remove_args = argparse.Namespace(path=args.path, remove=True)
        cmd_setup_claude(remove_args)
        # Remove MCP settings
        _configure_mcp_settings(remove=True)
        print("\nDone. flyto-indexer removed.")
        return None

    print(f"Setting up flyto-indexer for {project_path.name}...\n")

    # Step 1: Scan
    index_dir = project_path / ".flyto-index"
    if index_dir.exists():
        print(f"  Index exists at {index_dir}, skipping scan.")
    else:
        print("  [1/3] Scanning project...")
        scan_args = argparse.Namespace(
            path=str(project_path), full=False,
            name=project_path.name, output=None,
        )
        cmd_scan(scan_args)

    # Step 2: CLAUDE.md
    print("  [2/3] Writing CLAUDE.md...")
    claude_args = argparse.Namespace(path=args.path, remove=False)
    cmd_setup_claude(claude_args)

    # Step 3: MCP settings
    print("  [3/3] Configuring MCP server...")
    _configure_mcp_settings()

    print(f"\nDone! Restart Claude Code to activate.")
    print("Try: ask Claude to audit this project.")
    return None


def cmd_setup_claude(args):
    """Add flyto-indexer instructions to CLAUDE.md."""
    project_path = Path(args.path).resolve()
    claude_md = project_path / "CLAUDE.md"

    if args.remove:
        if not claude_md.exists():
            print("No CLAUDE.md found. Nothing to remove.")
            return None
        content = claude_md.read_text(encoding="utf-8")
        if CLAUDE_MARKER_BEGIN not in content:
            print("No flyto-indexer section found in CLAUDE.md. Nothing to remove.")
            return None
        lines = content.splitlines(keepends=True)
        new_lines = []
        inside = False
        for line in lines:
            if CLAUDE_MARKER_BEGIN in line:
                inside = True
                continue
            if CLAUDE_MARKER_END in line:
                inside = False
                continue
            if not inside:
                new_lines.append(line)
        new_content = "".join(new_lines).strip()
        if new_content:
            claude_md.write_text(new_content + "\n", encoding="utf-8")
            print(f"Removed flyto-indexer section from {claude_md}")
        else:
            claude_md.unlink()
            print(f"CLAUDE.md was empty after removal — deleted {claude_md}")
        return None

    # Install mode
    if claude_md.exists():
        content = claude_md.read_text(encoding="utf-8")
        if CLAUDE_MARKER_BEGIN in content:
            print("flyto-indexer section already exists in CLAUDE.md. Skipping.")
            print("Use --remove to remove it first, then re-run.")
            return None
        if not content.endswith("\n"):
            content += "\n"
        content += "\n" + CLAUDE_SECTION
        claude_md.write_text(content, encoding="utf-8")
    else:
        claude_md.write_text(CLAUDE_SECTION, encoding="utf-8")

    print(f"Added flyto-indexer instructions to {claude_md}")
    print("AI assistants will now use analyze_task before modifying shared code.")
    return None


def cmd_check(args):
    """CI-friendly impact check — exits non-zero when changes are risky."""
    from .engine import IndexEngine

    project_path = Path(args.path).resolve()
    project_name = project_path.name

    engine = IndexEngine(project_name, project_path)

    # Detect changed files
    changed_files = []
    if args.base:
        # Use git diff against base ref
        try:
            result = subprocess.run(
                ["git", "-C", str(project_path), "diff", "--name-only", f"{args.base}...HEAD"],
                capture_output=True, text=True, timeout=30,
            )
            if result.returncode == 0:
                changed_files = [f for f in result.stdout.strip().split("\n") if f]
            else:
                print(f"git diff failed: {result.stderr.strip()}", file=sys.stderr)
                sys.exit(1)
        except FileNotFoundError:
            print("git not found", file=sys.stderr)
            sys.exit(1)
        except subprocess.TimeoutExpired:
            print("git diff timed out", file=sys.stderr)
            sys.exit(1)
    else:
        # Use index staleness — re-scan and detect via incremental
        from .indexer.incremental import scan_directory_hashes

        extensions = []
        for scanner in engine.scanners:
            extensions.extend(scanner.supported_extensions)

        current_hashes = scan_directory_hashes(
            project_path, extensions,
            ignore_patterns=[
                "node_modules", "__pycache__", ".git", "dist", "build",
                ".venv", "venv", ".pytest_cache", ".flyto-index", ".flyto",
            ],
        )
        changes = engine.incremental.detect_changes(current_hashes)
        changed_files = changes.all_changed()

    # For each changed file, find symbols and compute impact
    total_affected = 0
    all_affected_files = set()
    symbol_details = []

    for rel_path in changed_files:
        # Find symbols defined in this file
        for sym_id, sym in engine.index.symbols.items():
            if sym.path == rel_path:
                chain = engine.index.get_impact_chain(sym_id, max_depth=3)
                affected_ids = []
                affected_paths = set()
                for level in chain.get("levels", []):
                    for sid in level.get("symbols", []):
                        affected_ids.append(sid)
                        if sid in engine.index.symbols:
                            affected_paths.add(engine.index.symbols[sid].path)

                if affected_ids:
                    symbol_details.append({
                        "symbol": sym_id,
                        "name": sym.name,
                        "path": sym.path,
                        "affected_count": len(affected_ids),
                        "affected_files": sorted(affected_paths),
                    })
                    total_affected += len(affected_ids)
                    all_affected_files.update(affected_paths)

    # Compute aggregate risk
    if total_affected >= 20:
        risk = "HIGH"
    elif total_affected >= 3:
        risk = "MEDIUM"
    else:
        risk = "LOW"

    risk_levels = {"LOW": 0, "MEDIUM": 1, "HIGH": 2}
    threshold_level = risk_levels[args.threshold.upper()]
    actual_level = risk_levels[risk]
    should_fail = actual_level >= threshold_level

    output = {
        "risk": risk,
        "changed_files": len(changed_files),
        "total_affected": total_affected,
        "affected_files": len(all_affected_files),
        "threshold": args.threshold.upper(),
        "pass": not should_fail,
        "symbols": symbol_details,
    }

    if hasattr(args, "as_json") and args.as_json:
        print(json.dumps(output, indent=2, ensure_ascii=False))
    else:
        print(f"Risk: {risk}")
        print(f"Changed files: {len(changed_files)}")
        print(f"Total affected symbols: {total_affected}")
        print(f"Affected files: {len(all_affected_files)}")
        print(f"Threshold: {args.threshold.upper()}")
        print()

        if symbol_details:
            for detail in symbol_details[:10]:
                print(f"  {detail['name']} ({detail['path']})")
                print(f"    → {detail['affected_count']} affected symbols in {len(detail['affected_files'])} files")
            if len(symbol_details) > 10:
                print(f"  ... and {len(symbol_details) - 10} more symbols")
            print()

        if should_fail:
            print(f"FAIL: risk {risk} >= threshold {args.threshold.upper()}")
        else:
            print(f"PASS: risk {risk} < threshold {args.threshold.upper()}")

    if should_fail:
        sys.exit(1)

    return None


if __name__ == "__main__":
    main()
