"""
Kodara CLI

Commands:
  kodara init     — Scan project, build memory, create .kodara/
  kodara ask      — Get relevant context for a question
  kodara watch    — Watch for changes, keep memory fresh
  kodara context  — Generate ARCHITECTURE.md overview
  kodara history  — Git history for a file
  kodara status   — Show what's indexed
  kodara team     — Share memory with your team
  kodara serve    — Start API server
"""

from __future__ import annotations

import json
import sys
import time
from pathlib import Path
from typing import Optional

import click

try:
    from rich.console import Console
    from rich.panel import Panel
    HAS_RICH = True
except ImportError:
    HAS_RICH = False

if HAS_RICH:
    import io
    import sys
    if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
    console = Console(soft_wrap=True)
else:
    console = None


def _print(msg: str, style: str = "") -> None:
    if HAS_RICH and console:
        console.print(msg, style=style)
    else:
        click.echo(msg)


def _out(msg: str) -> None:
    """Plain print that bypasses Rich entirely — reliable on Windows cp1251."""
    print(msg, flush=True)


class KodaraGroup(click.Group):
    """Custom Click Group — commands grouped by subscription tier."""

    FREE = [
        "init", "ask", "status", "watch", "stats",
        "history", "note", "snapshot",
        "install-hook", "uninstall-hook", "repos",
    ]
    PRO = [
        "impact", "context", "review", "diff",
        "why", "simulate-change", "heatmap",
        "dna", "learn", "evolve", "compare",
        "drift", "clone-pattern", "scan",
        "mcp", "backup", "restore", "backups",
        "login", "logout", "whoami",
    ]
    TEAM = [
        "onboard", "bus-factor", "debt",
        "arch-init", "check-architecture", "team",
    ]
    ENTERPRISE = [
        "ci-check", "init-ci",
    ]

    def format_commands(self, ctx, formatter):
        sections = [
            ("FREE — up to 200 files", self.FREE),
            ("PRO — $19/month (unlimited + cloud)", self.PRO),
            ("TEAM — $25/user/month", self.TEAM),
            ("ENTERPRISE — custom", self.ENTERPRISE),
        ]
        for section_title, names in sections:
            cmds = []
            for name in names:
                cmd = self.commands.get(name)
                if cmd is None:
                    continue
                help_text = cmd.get_short_help_str(limit=45)
                cmds.append((name, help_text))
            if cmds:
                with formatter.section(section_title):
                    formatter.write_dl(cmds)

        # OTHER bucket for anything uncategorized
        all_categorized = set(self.FREE + self.PRO + self.TEAM + self.ENTERPRISE)
        other = [
            (name, cmd.get_short_help_str(limit=45))
            for name, cmd in sorted(self.commands.items())
            if name not in all_categorized
        ]
        if other:
            with formatter.section("OTHER"):
                formatter.write_dl(other)


@click.group(cls=KodaraGroup)
@click.version_option("0.2.5", prog_name="kodara")
def main():
    """Kodara — Memory Layer for AI Development.

    Index once. Ask anything. Your AI always knows the project.
    """
    pass


# ── kodara init ───────────────────────────────────────────────────────────────


@main.command()
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--no-summaries", is_flag=True, help="Skip summary generation")
@click.option("--full", is_flag=True, help="Force full re-index (ignore cache)")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def init(path, no_summaries, full, as_json):
    """Scan project, analyse code, build memory in .kodara/

    Run once per project. Use 'kodara watch' to keep it fresh.
    """
    resolved = Path(path).resolve()

    # Capture previous snapshot BEFORE re-indexing (auto_snapshot runs inside _run_init)
    _prev_snapshot = None
    if not as_json:
        try:
            from kodara.snapshots.store import SnapshotStore
            _snap_store = SnapshotStore(resolved / ".kodara")
            _snaps = _snap_store.list()
            if _snaps:
                _prev_snapshot = _snap_store.load(_snaps[0].filename)
        except Exception:
            pass

    _run_init(resolved, no_summaries=no_summaries, full=full, as_json=as_json, verbose=not as_json)

    # Drift check: compare new index against OLD snapshot
    if not as_json and _prev_snapshot:
        try:
            from kodara.drift.detector import detect_drift
            from kodara.memory.store import MemoryStore
            _idx = MemoryStore(resolved).load()
            if _idx:
                drift = detect_drift(_idx, _prev_snapshot)
                if drift.has_alerts:
                    click.echo(f"\nDrift: {len(drift.alerts)} alert(s) detected. Run `kodara drift` for details.")
        except Exception:
            pass


# ── kodara watch ──────────────────────────────────────────────────────────────


@main.command()
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--no-summaries", is_flag=True)
@click.option("--context", "do_context", is_flag=True,
              help="Regenerate ARCHITECTURE.md on every change")
def watch(path, no_summaries, do_context):
    """Watch for file changes and automatically update memory."""
    from kodara.incremental.watcher import FileWatcher

    root = Path(path).resolve()
    store = _store(root)

    if not store.exists():
        _print(f"[yellow]Not initialised. Running kodara init first...[/yellow]")
        _run_init(root, no_summaries=no_summaries, full=False, as_json=False, verbose=True)

    _print(f"\n[bold cyan]Kodara Watch[/bold cyan] — [bold]{root.name}[/bold]")
    if do_context:
        _print("  [dim]ARCHITECTURE.md will update on every change[/dim]")
    _print("  Press Ctrl+C to stop.\n")

    def on_change(changed: list[str], deleted: list[str]) -> None:
        if changed:
            _print(f"  [yellow]~[/yellow] {', '.join(Path(f).name for f in changed[:4])}")
        if deleted:
            _print(f"  [red]-[/red] {', '.join(Path(f).name for f in deleted[:3])}")
        try:
            _run_init(root, no_summaries=no_summaries, full=False, as_json=False, verbose=False)
            _print("  [green]OK[/green] Memory updated")
            if do_context:
                _regenerate_context(root)
        except Exception as e:
            _print(f"  [red]Error:[/red] {e}")

    watcher = FileWatcher(str(root), on_change)
    watcher.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        _print("\n[dim]Stopped.[/dim]")
        watcher.stop()


# ── kodara ask ────────────────────────────────────────────────────────────────


@main.command()
@click.argument("query")
@click.option("--path", default=".", type=click.Path(exists=True))
@click.option("--max-modules", default=8, show_default=True)
@click.option("--depth", default=1, show_default=True)
@click.option("--json", "as_json", is_flag=True, help="Machine-readable JSON output")
@click.option("--verbose", "-v", is_flag=True, help="Show full code snippets")
def ask(query, path, max_modules, depth, as_json, verbose):
    """Get relevant context for a question about your codebase."""
    from kodara.engine_client import query_context

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print(f"[red]Error:[/red] Project not initialised. Run: kodara init")
        sys.exit(1)

    try:
        result = query_context(idx.modules, idx.graph_edges, query, max_modules, depth)
    except RuntimeError as e:
        _print(f"[red]Error:[/red] {e}")
        sys.exit(1)

    r_modules = result["modules"]
    r_relevant = result["relevant_files"]
    r_token_est = result["token_estimate"]
    r_total = result["total_modules_searched"]
    r_graph = result["graph_fragment"]

    if as_json:
        click.echo(json.dumps({
            "query": query,
            "modules": r_modules,
            "relevant_files": r_relevant,
            "token_estimate": r_token_est,
            "graph_fragment": r_graph,
        }, indent=2))
        return

    from kodara.snippets.extractor import extract_snippets
    from kodara.stats.tracker import StatsTracker

    # Record usage for stats
    try:
        tracker = StatsTracker(store.kodara_dir)
        tracker.record(
            query=query,
            tokens_used=r_token_est,
            total_modules=r_total,
            modules_selected=len(r_modules),
            index_total_files=idx.file_count,
            relevant_files=r_relevant,
        )
    except Exception:
        pass

    # Attach notes to modules in context
    from kodara.notes.store import NotesStore
    notes_store = NotesStore(store.kodara_dir)

    # Context block: ready to paste into any AI
    output_lines: list[str] = [f"\n# Context: {query}\n"]

    for mod in r_modules:
        file_p = mod.get("file", "")
        summary = mod.get("summary", "")
        classes = mod.get("classes", [])
        fns = mod.get("functions", [])[:8]
        deps = mod.get("dependencies", [])
        git = mod.get("git", {})
        language = mod.get("language", "")

        output_lines.append(f"## {file_p}")
        if summary:
            output_lines.append(f"_{summary}_")
        if deps:
            dep_names = ", ".join(Path(d).name for d in deps[:4])
            output_lines.append(f"Depends on: {dep_names}")
        if git.get("last_author"):
            output_lines.append(
                f"Last change: {git['last_author']} — \"{git.get('last_message','')}\" ({git.get('last_date','')})"
            )

        # Show notes for this file
        file_notes = notes_store.get(file_p)
        for note in file_notes[:2]:
            output_lines.append(f"> NOTE ({note.author}, {note.date}): {note.text}")

        # Extract actual code snippets (only in verbose mode)
        if verbose:
            top_symbols = (classes + fns)[:6]
            snippet = extract_snippets(file_p, top_symbols, language)
            if snippet:
                output_lines.append(f"```{language}")
                output_lines.append(snippet)
                output_lines.append("```")

        output_lines.append("")

    # Dependency flow
    edges = r_graph.get("edges", [])
    if edges:
        flow = " -> ".join(
            Path(e.get("from", "")).stem for e in edges[:4]
        )
        output_lines.append(f"_Flow: {flow}_\n")

    pct = 0
    if r_total:
        pct = 100 * (1 - len(r_modules) / r_total)
    output_lines.append(
        f"[{len(r_modules)}/{r_total} modules · ~{r_token_est:,} tokens · {pct:.0f}% reduction]"
    )

    click.echo("\n".join(output_lines))


# ── kodara impact ────────────────────────────────────────────────────────────


@main.command()
@click.argument("file", required=False, default=None)
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--json", "as_json", is_flag=True)
def impact(file, path, as_json):
    """Show what breaks if you change a file.

    \b
    Example:
      kodara impact auth/middleware.py
      kodara impact              (auto-selects most critical file)
    """
    from kodara.engine_client import query_impact

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print(f"[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    if file is None:
        # Auto-select the hub module (most imported = most critical)
        best = max(
            idx.modules.items(),
            key=lambda kv: len(kv[1].get("dependents", [])),
            default=None,
        )
        if not best:
            _print("[red]No modules indexed.[/red]")
            sys.exit(1)
        file = best[0]
        _print(f"[dim]Auto-selected: {file}[/dim]\n")

    try:
        result = query_impact(idx.modules, file)
    except RuntimeError as e:
        _print(f"[red]{e}[/red]")
        sys.exit(1)
    except ValueError as e:
        _print(f"[red]{e}[/red]")
        sys.exit(1)

    if as_json:
        click.echo(json.dumps(result, indent=2))
        return

    # Risk badge
    risk_colors = {"NONE": "green", "LOW": "green", "MEDIUM": "yellow", "HIGH": "red", "CRITICAL": "bold red"}
    color = risk_colors.get(result["risk_level"], "white")

    _print(f"\n[bold]Impact Analysis[/bold] — {result['target_module']}")
    if result["target_summary"]:
        _print(f"[dim]{result['target_summary']}[/dim]")
    _print(f"\nRisk: [{color}]{result['risk_level']}[/{color}]  {result['risk_reason']}\n")

    if result["total_affected"] == 0:
        _print("[green]No other modules import this file. Safe to change.[/green]\n")
        return

    if result["direct"]:
        _print(f"[bold]Direct dependents[/bold] ({len(result['direct'])}) — will break immediately:")
        for m in result["direct"][:10]:
            hub_tag = " [yellow][hub][/yellow]" if m["is_hub"] else ""
            _print(f"  {m['module_name']}{hub_tag}")
            if m["summary"]:
                _print(f"    [dim]{m['summary']}[/dim]")
        if len(result["direct"]) > 10:
            _print(f"  [dim]... and {len(result['direct']) - 10} more (use --json for full list)[/dim]")
        _print("")

    if result["indirect"]:
        _print(f"[bold]Indirect dependents[/bold] ({len(result['indirect'])}) — affected through the chain:")
        for m in result["indirect"][:8]:
            hub_tag = " [yellow][hub][/yellow]" if m["is_hub"] else ""
            _print(f"  {'  ' * (m['distance'] - 1)}{m['module_name']} (depth {m['distance']}){hub_tag}")
        if len(result["indirect"]) > 8:
            _print(f"  [dim]... and {len(result['indirect']) - 8} more[/dim]")
        _print("")

    if result["critical_paths"]:
        _print("[bold]Critical chains:[/bold]")
        for chain in result["critical_paths"]:
            names = [Path(p).stem for p in chain]
            _print(f"  {' → '.join(names)}")
        _print("")


# ── kodara onboard ────────────────────────────────────────────────────────────


@main.command()
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--json", "as_json", is_flag=True)
def onboard(path, as_json):
    """Generate a reading guide for a new developer.

    Shows where to start and in what order to explore the codebase.
    """
    from kodara.onboard.guide import generate

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print(f"[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    guide = generate(idx)

    if as_json:
        import dataclasses
        click.echo(json.dumps(dataclasses.asdict(guide), indent=2))
        return

    _out(f"Onboarding Guide — {guide.project_name}\n")
    _out(f"{guide.architecture_summary}\n")

    if guide.top_contributors:
        contribs = ", ".join(f"{c['name']} ({c['commits']} commits)" for c in guide.top_contributors[:3])
        _out(f"Contributors: {contribs}\n")

    layer_icons = {"entry": ">>", "core": "**", "foundation": "--", "utility": "  "}
    layer_labels = {"entry": "ENTRY", "core": "CORE ", "foundation": "BASE ", "utility": "UTIL "}

    _out("Reading order:\n")
    for step in guide.steps:
        icon = layer_icons.get(step.layer, "  ")
        label = layer_labels.get(step.layer, "     ")
        module_name = step.module_name.replace('\r', '').replace('\n', '')
        step_path = step.path.replace('\r', '').replace('\n', '')
        step_reason = step.reason.replace('\r', '').replace('\n', '')
        _out(f"")
        _out(f"  {step.order:2}. {label} {icon} {module_name}")
        _out(f"          {step_path}")
        if step.summary:
            raw = step.summary.replace('\r', ' ').replace('\n', ' ')
            summary = raw if len(raw) <= 90 else raw[:87] + "..."
            _out(f"          {summary}")
        _out(f"          {step_reason}")


# ── kodara note ──────────────────────────────────────────────────────────────


@main.group(invoke_without_command=True, context_settings={"allow_interspersed_args": True})
@click.argument("file", default="")
@click.argument("text", default="")
@click.option("--path", default=".", type=click.Path(exists=True))
@click.option("--list", "do_list", is_flag=True, help="List all notes")
@click.option("--search", default="", help="Search notes")
@click.option("--delete", "del_index", default=-1, type=int,
              help="Delete note by index (0=newest)")
@click.pass_context
def note(ctx, file, text, path, do_list, search, del_index):
    """Add or view developer notes on files.

    \b
    Examples:
      kodara note auth/middleware.py "Rewritten after security incident"
      kodara note auth/middleware.py          # show notes for file
      kodara note --list                      # show all notes
      kodara note --search "security"         # search notes
      kodara note auth/middleware.py --delete 0   # delete newest note
    """
    from kodara.notes.store import NotesStore

    root = Path(path).resolve()
    store = _store(root)

    if not store.exists():
        _print(f"[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    idx = store.load()
    ns = NotesStore(store.kodara_dir)

    # --list: show all notes
    if do_list or (not file and not search):
        all_notes = ns.get_all()
        if not all_notes:
            _print("\n[dim]No notes yet.[/dim]")
            _print('[dim]Add one: kodara note <file> "your note"[/dim]\n')
            return
        _print(f"\n[bold]All Notes[/bold] ({sum(len(v) for v in all_notes.values())} total)\n")
        for fkey, fnotes in all_notes.items():
            _print(f"[bold cyan]{Path(fkey).name}[/bold cyan]  [dim]{fkey}[/dim]")
            for i, n in enumerate(fnotes):
                _print(f"  [{i}] [dim]{n.author} · {n.date}[/dim]")
                _print(f"      {n.text}")
            _print("")
        return

    # --search
    if search:
        results = ns.search(search)
        if not results:
            _print(f"\n[dim]No notes matching '{search}'[/dim]\n")
            return
        _print(f"\n[bold]Notes matching '{search}'[/bold]\n")
        for fkey, n in results:
            _print(f"  [cyan]{Path(fkey).name}[/cyan]  [dim]{n.date} · {n.author}[/dim]")
            _print(f"  {n.text}\n")
        return

    # Resolve file key
    file_key = ns.resolve_key(file, idx)

    # --delete
    if del_index >= 0:
        if ns.delete(file_key, del_index):
            _print(f"[green]Note deleted.[/green]")
        else:
            _print(f"[red]Note not found.[/red]")
        return

    # Show notes for file (no text given)
    if not text:
        fnotes = ns.get(file_key)
        if not fnotes:
            _print(f"\n[dim]No notes for {Path(file_key).name}[/dim]")
            _print(f'[dim]Add one: kodara note {file} "your note"[/dim]\n')
            return
        _print(f"\n[bold cyan]{Path(file_key).name}[/bold cyan]  [dim]{file_key}[/dim]\n")
        for i, n in enumerate(fnotes):
            _print(f"  [{i}] [dim]{n.author} · {n.date}[/dim]")
            _print(f"      {n.text}")
        _print("")
        return

    # Add note
    n = ns.add(file_key, text)
    _print(f"\n[bold green]Note saved[/bold green] → {Path(file_key).name}")
    _print(f"[dim]{n.author} · {n.date}[/dim]")
    _print(f"{n.text}\n")


# ── kodara snapshot ───────────────────────────────────────────────────────────


@main.group(invoke_without_command=True)
@click.option("--path", default=".", type=click.Path(exists=True))
@click.pass_context
def snapshot(ctx, path):
    """Capture or compare project memory snapshots.

    \b
    Commands:
      kodara snapshot              # create snapshot now
      kodara snapshot list         # show all snapshots
      kodara snapshot diff 2024-01 # compare to a past snapshot
    """
    if ctx.invoked_subcommand is None:
        # Default: create a snapshot
        from kodara.snapshots.store import SnapshotStore

        root = Path(path).resolve()
        store = _store(root)
        idx = store.load()

        if not idx:
            _print(f"[red]Not initialised.[/red] Run: kodara init")
            sys.exit(1)

        snap_store = SnapshotStore(store.kodara_dir)
        filename = snap_store.create(idx)
        _print(f"\n[bold green]Snapshot saved:[/bold green] {filename}")
        _print(f"[dim]{idx.file_count} files · {len(idx.modules)} modules[/dim]\n")


@snapshot.command("list")
@click.option("--path", default=".", type=click.Path(exists=True))
def snapshot_list(path):
    """List all snapshots."""
    from kodara.snapshots.store import SnapshotStore

    root = Path(path).resolve()
    store = _store(root)
    snap_store = SnapshotStore(store.kodara_dir)
    snaps = snap_store.list()

    if not snaps:
        _print("\n[dim]No snapshots yet.[/dim]")
        _print("[dim]Run: kodara snapshot[/dim]\n")
        return

    _print(f"\n[bold]Snapshots[/bold] ({len(snaps)})\n")
    for s in snaps:
        langs = ", ".join(s.languages.keys())
        _print(f"  [cyan]{s.timestamp}[/cyan]  {s.file_count} files  {langs}")
        _print(f"  [dim]{s.filename}[/dim]")
    _print("")


@snapshot.command("diff")
@click.argument("date")
@click.option("--path", default=".", type=click.Path(exists=True))
def snapshot_diff(date, path):
    """Compare current state to a past snapshot.

    \b
    Examples:
      kodara snapshot diff 2024-01-15
      kodara snapshot diff 2024-01
    """
    from kodara.snapshots.store import SnapshotStore

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print(f"[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    snap_store = SnapshotStore(store.kodara_dir)

    try:
        result = snap_store.compare(idx, date)
    except ValueError as e:
        _print(f"[red]{e}[/red]")
        sys.exit(1)

    _print(f"\n[bold]Snapshot Diff[/bold] — since {result['snapshot_date']}\n")
    _print(f"  {result['summary']}\n")

    if result["added_modules"]:
        _print(f"[green]Added ({len(result['added_modules'])}):[/green]")
        for m in result["added_modules"][:10]:
            _print(f"  + {Path(m).stem}")
        _print("")

    if result["removed_modules"]:
        _print(f"[red]Removed ({len(result['removed_modules'])}):[/red]")
        for m in result["removed_modules"][:10]:
            _print(f"  - {Path(m).stem}")
        _print("")

    if result["changed_modules"]:
        _print(f"[yellow]Modified ({len(result['changed_modules'])}):[/yellow]")
        for m in result["changed_modules"][:10]:
            _print(f"  ~ {Path(m).stem}")
        _print("")


# ── kodara stats ─────────────────────────────────────────────────────────────


@main.command()
@click.option("--path", default=".", type=click.Path(exists=True))
@click.option("--json", "as_json", is_flag=True)
def stats(path, as_json):
    """Show token savings and money saved by using Kodara."""
    from kodara.stats.tracker import StatsTracker

    root = Path(path).resolve()
    store = _store(root)

    if not store.exists():
        _print(f"[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    tracker = StatsTracker(store.kodara_dir)
    s = tracker.summary()

    if as_json:
        click.echo(json.dumps(s, indent=2))
        return

    if s.get("total_entries", 0) == 0:
        _print("\n[dim]No usage recorded yet. Run kodara ask first.[/dim]\n")
        return

    price = s.get("price_per_1m", 15.0)
    _print(f"\n[bold cyan]Kodara Stats[/bold cyan] — {root.name}")
    _print(f"[dim]Price: ${price}/1M tokens (set KODARA_PRICE_PER_1M to change)[/dim]\n")

    def _row(label: str, data: dict) -> None:
        if not data.get("queries"):
            return
        q = data["queries"]
        saved_t = data.get("tokens_saved", 0)
        saved_m = data.get("money_saved", 0.0)
        used_t = data.get("tokens_used", 0)
        pct = int(saved_t / (saved_t + used_t) * 100) if (saved_t + used_t) else 0
        click.echo(f"  {label}")
        click.echo(f"    Queries:      {q}")
        click.echo(f"    Tokens saved: {saved_t:,}  ({pct}% reduction)")
        click.echo(f"    Money saved:  ${saved_m:.2f}")
        click.echo("")

    _row("Today", s.get("today", {}))
    _row("This month", s.get("this_month", {}))
    _row("All time", s.get("all_time", {}))

    all_time = s.get("all_time", {})
    total_saved = all_time.get("money_saved", 0.0)
    if total_saved >= 1:
        click.echo(f"Total saved: ${total_saved:.2f}\n")


# ── kodara review ─────────────────────────────────────────────────────────────


@main.command()
@click.option("--path", default=".", type=click.Path(exists=True))
@click.option("--strict", is_flag=True,
              help="Exit with code 1 if HIGH or CRITICAL risk (for pre-commit hooks)")
@click.option("--all-changes", "all_changes", is_flag=True,
              help="Include unstaged changes too (default: staged only)")
def review(path, strict, all_changes):
    """Analyse impact of your current changes before committing.

    \b
    Use as a pre-commit hook:
      echo 'kodara review --strict' >> .git/hooks/pre-commit
      chmod +x .git/hooks/pre-commit
    """
    from kodara.diff.analyzer import analyze_diff

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print(f"[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    base = "HEAD"
    result = analyze_diff(idx, str(root), base=base, staged_only=not all_changes)

    if not result.changed_files:
        _print("\n[dim]No staged changes found.[/dim]")
        _print("[dim]Use --all-changes to include unstaged changes.[/dim]\n")
        return

    _print_diff_result(result, root)

    if strict and result.overall_risk in ("HIGH", "CRITICAL"):
        sys.exit(1)


# ── kodara diff ───────────────────────────────────────────────────────────────


@main.command()
@click.argument("base", default="main")
@click.option("--path", default=".", type=click.Path(exists=True))
@click.option("--json", "as_json", is_flag=True)
def diff(base, path, as_json):
    """Show impact of all changes vs a branch or commit.

    \b
    Examples:
      kodara diff main
      kodara diff HEAD~3
      kodara diff origin/main
    """
    from kodara.diff.analyzer import analyze_diff

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print(f"[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    result = analyze_diff(idx, str(root), base=base)

    if as_json:
        import dataclasses
        # ImpactResult inside FileChange needs special handling
        out = {
            "base": result.base,
            "overall_risk": result.overall_risk,
            "total_affected_modules": result.total_affected_modules,
            "risk_distribution": result.risk_distribution,
            "summary": result.summary,
            "files": [
                {
                    "path": fc.path,
                    "status": fc.status,
                    "in_index": fc.in_index,
                    "risk": fc.impact.risk_level if fc.impact else "N/A",
                    "affected": fc.impact.total_affected if fc.impact else 0,
                }
                for fc in result.changed_files
            ],
        }
        click.echo(json.dumps(out, indent=2))
        return

    _print_diff_result(result, root)


# ── Shared diff printer ───────────────────────────────────────────────────────


def _print_diff_result(result, root: Path) -> None:
    risk_colors = {
        "NONE": "green", "LOW": "green",
        "MEDIUM": "yellow", "HIGH": "red", "CRITICAL": "bold red"
    }
    status_labels = {"M": "modified", "A": "added", "D": "deleted", "R": "renamed"}

    color = risk_colors.get(result.overall_risk, "white")
    _print(f"\n[bold]Diff Analysis[/bold] vs [dim]{result.base}[/dim]")
    _print(f"Overall risk: [{color}]{result.overall_risk}[/{color}]\n")

    if not result.changed_files:
        _print("[dim]No changes found.[/dim]\n")
        return

    for fc in result.changed_files:
        status = status_labels.get(fc.status, fc.status)
        if fc.status == "D" or not fc.in_index:
            tag = "[dim](not in index)[/dim]" if not fc.in_index else "[dim](deleted)[/dim]"
            _print(f"  {fc.path}  {tag}")
            continue

        imp = fc.impact
        if not imp:
            continue

        risk_color = risk_colors.get(imp.risk_level, "white")
        _print(
            f"  [{risk_color}]{imp.risk_level:8}[/{risk_color}]  "
            f"[bold]{imp.target_module}[/bold]  "
            f"[dim]({status})[/dim]"
        )
        if imp.total_affected:
            direct_n = len(imp.direct)
            indirect_n = len(imp.indirect)
            _print(f"             Affects: {imp.total_affected} modules "
                   f"({direct_n} direct, {indirect_n} indirect)")
        if imp.risk_level in ("HIGH", "CRITICAL") and imp.direct:
            top = ", ".join(m.module_name for m in imp.direct[:3])
            _print(f"             Direct:  {top}")
        _print("")

    if result.total_affected_modules:
        _print(f"[dim]Total blast radius: {result.total_affected_modules} unique modules affected[/dim]")

    # Risk breakdown
    non_zero = {k: v for k, v in result.risk_distribution.items() if v > 0}
    if non_zero:
        breakdown = "  ".join(f"{k}: {v}" for k, v in non_zero.items())
        _print(f"[dim]Risk breakdown: {breakdown}[/dim]")
    _print("")


# ── kodara context ────────────────────────────────────────────────────────────


@main.command("context")
@click.argument("path", default=".", type=click.Path(exists=True))
def context_cmd(path):
    """Generate ARCHITECTURE.md — human-readable project overview."""
    root = Path(path).resolve()
    _regenerate_context(root, verbose=True)


# ── kodara status ─────────────────────────────────────────────────────────────


@main.command()
@click.argument("path", default=".", type=click.Path(exists=True))
def status(path):
    """Show index status for this project."""
    from datetime import datetime

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print(f"\n[yellow]Not initialised.[/yellow] Run: [bold]kodara init[/bold]\n")
        return

    age = time.time() - idx.indexed_at
    age_str = f"{int(age/60)}m ago" if age < 3600 else f"{int(age/3600)}h ago"
    langs = ", ".join(f"{l}({c})" for l, c in idx.language_counts.items())

    _print(f"\n[bold cyan]{root.name}[/bold cyan]  [green]indexed[/green]")
    _print(f"  Files:    {idx.file_count}  |  Languages: {langs}")
    _print(f"  Updated:  {age_str}  ({datetime.fromtimestamp(idx.indexed_at).strftime('%Y-%m-%d %H:%M')})")
    _print(f"  Memory:   {store.kodara_dir}\n")


# ── kodara history ────────────────────────────────────────────────────────────


@main.command()
@click.argument("file", required=False, default=None, type=click.Path())
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--json", "as_json", is_flag=True)
def history(file, path, as_json):
    """Show git history and authorship for a file.

    \b
    Example:
      kodara history src/app.py
      kodara history             (auto-selects most active file)
    """
    from kodara.git.history import is_git_repo, collect_file_histories, get_recent_commits

    root = Path(path).resolve()

    if not is_git_repo(str(root)):
        _print(f"[yellow]No git repository at {root}[/yellow]")
        sys.exit(1)

    store = _store(root)
    idx = store.load()
    git_info = {}

    if file is None:
        if idx:
            # Try: file with most commits from index git data
            best = max(
                idx.modules.items(),
                key=lambda kv: kv[1].get("git", {}).get("commit_count", 0),
                default=None,
            )
            max_commits = best[1].get("git", {}).get("commit_count", 0) if best else 0

            # Fallback: if no git data in index, pick hub module (most imported)
            if max_commits == 0:
                best = max(
                    idx.modules.items(),
                    key=lambda kv: len(kv[1].get("dependents", [])),
                    default=None,
                )

            if best:
                file = best[0]
                commits = best[1].get("git", {}).get("commit_count", 0)
                label = f"{commits} commits" if commits > 0 else "most imported"
                _print(f"Auto-selected: {file} ({label})")
                git_info = best[1].get("git", {})
        if file is None:
            _print("[red]No indexed files found.[/red] Run: kodara init")
            sys.exit(1)

    file_path = Path(file).resolve()

    if not git_info:
        if idx and str(file_path) in idx.modules:
            git_info = idx.modules[str(file_path)].get("git", {})

    if not git_info:
        all_info = collect_file_histories(str(root))
        git_info = all_info.get(str(file_path), {})

    if as_json:
        click.echo(json.dumps({"file": str(file_path), "git": git_info}, indent=2))
        return

    if not git_info:
        _print(f"[yellow]No git history found for {file_path.name}[/yellow]")
        return

    _out(f"Git History - {file_path.name}")
    _out(f"")
    _out(f"  Commits:     {git_info.get('commit_count', 0)}")
    _out(f"  Last commit: {git_info.get('last_commit', '')}  {git_info.get('last_date', '')}")
    _out(f"  Last author: {git_info.get('last_author', '')}")
    _out(f"  Last change: {git_info.get('last_message', '')}")

    for a in git_info.get("top_authors", []):
        _out(f"  {a['name']}  ({a['commits']} commits)")
    _out(f"")


# ── kodara team ───────────────────────────────────────────────────────────────


@main.group()
def team():
    """Share project memory with your team."""
    pass


@team.command("config")
@click.option("--shared-dir", required=True)
@click.option("--path", default=".", type=click.Path(exists=True))
def team_config(shared_dir, path):
    """Set the shared directory for team sync."""
    from kodara.team.sync import set_shared_dir
    resolved = set_shared_dir(shared_dir, Path(path).resolve())
    _print(f"\n[bold green]OK[/bold green] Shared dir: {resolved}\n")


@team.command("push")
@click.option("--path", default=".", type=click.Path(exists=True))
def team_push(path):
    """Push this project's memory to the shared directory."""
    from kodara.team.sync import push
    root = Path(path).resolve()
    try:
        dst = push(root)
        _print(f"\n[bold green]Pushed[/bold green] -> {dst}\n")
    except ValueError as e:
        _print(f"[red]Error:[/red] {e}")
        sys.exit(1)


@team.command("pull")
@click.option("--path", default=".", type=click.Path(exists=True))
def team_pull(path):
    """Pull team memory into this project."""
    from kodara.team.sync import pull
    root = Path(path).resolve()
    try:
        pulled = pull(root)
        _print(f"\n[bold green]Pulled {len(pulled)} index(es)[/bold green]\n")
    except ValueError as e:
        _print(f"[red]Error:[/red] {e}")
        sys.exit(1)


@team.command("status")
@click.option("--path", default=".", type=click.Path(exists=True))
def team_status(path):
    """Show sync status."""
    from kodara.team.sync import status
    st = status(Path(path).resolve())
    _print(f"\n  Shared dir: [dim]{st.get('shared_dir') or 'not configured'}[/dim]")
    _print(f"  Local:  {'indexed' if st.get('local_exists') else 'not indexed'}")
    _print(f"  Shared: {'present' if st.get('shared_exists') else 'not pushed'}\n")


# ── kodara serve ──────────────────────────────────────────────────────────────


@main.command("install-hook")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
def install_hook(path):
    """Install git post-commit hook to auto-update Kodara index after each commit."""
    import stat
    root = Path(path).resolve()
    git_dir = root / ".git"
    if not git_dir.is_dir():
        _out(f"  Error: no .git directory found in {root}")
        _out(f"  Run this command from the root of a git repository.")
        raise SystemExit(1)

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

    hook_script = "#!/bin/sh\nkodara init --json 2>/dev/null || true\n"

    if hook_path.exists():
        existing = hook_path.read_text()
        if "kodara init --json" in existing:
            _out(f"  Kodara hook already installed.")
            _out(f"  Location: {hook_path}")
            return
        # Append to existing hook
        with open(hook_path, "a") as f:
            f.write(f"\n# Kodara — auto-update index\nkodara init --json 2>/dev/null || true\n")
    else:
        hook_path.write_text(hook_script)

    # Make executable
    hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

    _out(f"")
    _out(f"  Kodara hook installed.")
    _out(f"  After every git commit — index updates automatically.")
    _out(f"")
    _out(f"  Location: {hook_path}")
    _out(f"  Remove:   kodara uninstall-hook")
    _out(f"")


@main.command("uninstall-hook")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
def uninstall_hook(path):
    """Remove Kodara git post-commit hook."""
    root = Path(path).resolve()
    hook_path = root / ".git" / "hooks" / "post-commit"

    if not hook_path.exists():
        _out(f"  No post-commit hook found.")
        return

    content = hook_path.read_text()
    if "kodara update" not in content:
        _out(f"  Kodara hook not found in post-commit.")
        return

    lines = content.splitlines(keepends=True)
    filtered = [
        l for l in lines
        if "kodara init --json" not in l and "Kodara — auto-update" not in l
    ]
    # If only shebang left — remove file entirely
    remaining = "".join(filtered).strip()
    if remaining in ("", "#!/bin/sh"):
        hook_path.unlink()
        _out(f"  Kodara hook removed. File deleted (was empty).")
    else:
        hook_path.write_text("".join(filtered))
        _out(f"  Kodara hook removed.")

    _out(f"")


@main.command()
@click.option("--host", default="127.0.0.1", show_default=True)
@click.option("--port", default=8742, show_default=True)
@click.option("--path", default=".", type=click.Path(exists=True))
def serve(host, port, path):
    """Start the Kodara API server."""
    _print(f"\n[bold cyan]Kodara API[/bold cyan] — http://{host}:{port}")
    from kodara.api.server import start_server
    start_server(host=host, port=port)


# ── kodara arch-init ───────────────────────────────────────────────────────────


@main.command("arch-init")
@click.argument("path", default=".", type=click.Path(exists=True))
def arch_init(path):
    """Create .kodara/architecture.yml — auto-detect layers from project structure.

    \b
    Detects standard layer directories:
      api/, routes/, views/       → layer: api
      service/, services/         → layer: service
      repository/, models/, db/   → layer: repository
      utils/, helpers/, common/   → layer: utils

    Edit .kodara/architecture.yml to customise rules after generation.
    """
    try:
        import yaml
    except ImportError:
        _out("  Error: pyyaml is required. Install with: pip install pyyaml")
        sys.exit(1)

    from kodara.architecture.guard import generate_config

    root = Path(path).resolve()
    kodara_dir = root / ".kodara"

    if not kodara_dir.exists():
        _out("  Not initialised. Run: kodara init")
        sys.exit(1)

    config, config_path = generate_config(root, kodara_dir)
    layers = config.get("architecture", {})

    if not layers:
        _out("")
        _out("  No standard layer directories found.")
        _out("  Create .kodara/architecture.yml manually.")
        _out("")
        _out("  Example:")
        _out("    architecture:")
        _out("      api:")
        _out("        patterns: [\"api/*\"]")
        _out("        can_import: [service, utils]")
        _out("")
        return

    with open(config_path, "w", encoding="utf-8") as f:
        yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)

    _out("")
    _out(f"  Architecture config created: {config_path}")
    _out("")
    _out(f"  Detected {len(layers)} layer(s):")
    for name, cfg in layers.items():
        allowed = cfg.get("can_import", [])
        allowed_str = ", ".join(allowed) if allowed else "(none)"
        _out(f"    {name:12} can import: {allowed_str}")
    _out("")
    _out("  Edit .kodara/architecture.yml to customise rules.")
    _out("  Then run: kodara check-architecture")
    _out("")


# ── kodara check-architecture ─────────────────────────────────────────────────


@main.command("check-architecture")
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def check_architecture(path, as_json):
    """Check project imports against architecture layer rules.

    \b
    Reads rules from .kodara/architecture.yml
    Run 'kodara arch-init' to generate the config first.

    \b
    Example:
      kodara arch-init
      kodara check-architecture
    """
    from kodara.architecture.guard import check as guard_check

    root = Path(path).resolve()
    kodara_dir = root / ".kodara"
    store = _store(root)
    idx = store.load()

    if not idx:
        _out("  Not initialised. Run: kodara init")
        sys.exit(1)

    config_path = kodara_dir / "architecture.yml"
    if not config_path.exists():
        _out("")
        _out("  No architecture config found.")
        _out("  Run: kodara arch-init")
        _out("")
        sys.exit(0)

    try:
        result = guard_check(idx, kodara_dir)
    except ImportError as e:
        _out(f"  Error: {e}")
        sys.exit(1)

    if as_json:
        out = {
            "clean": result.is_clean,
            "checked_edges": result.checked_edges,
            "violation_count": len(result.violations),
            "violations": [
                {
                    "from": v.from_file,
                    "to": v.to_file,
                    "from_layer": v.from_layer,
                    "to_layer": v.to_layer,
                    "rule": v.rule,
                }
                for v in result.violations
            ],
        }
        click.echo(json.dumps(out, indent=2))
        return

    _out("")
    _out(f"  Architecture Check — {root.name}")
    _out(f"  {'=' * (22 + len(root.name))}")

    if result.is_clean:
        _out(f"  OK  No violations found. ({result.checked_edges} edges checked)")
        _out("")
        return

    _out(f"  {len(result.violations)} violation(s) found  ({result.checked_edges} edges checked)")
    _out("")

    for v in result.violations:
        from_short = Path(v.from_file).as_posix()
        to_short = Path(v.to_file).as_posix()
        _out(f"  VIOLATION  [{v.from_layer}] -> [{v.to_layer}]")
        _out(f"    File:  {from_short}")
        _out(f"    Imports: {to_short}")
        _out(f"    Rule:  {v.rule}")
        _out("")

    _out(f"  Run 'kodara arch-init' to review layer configuration.")
    _out("")


# ── kodara ci-check ───────────────────────────────────────────────────────────


@main.command("ci-check")
@click.argument("path", default=".", type=click.Path(exists=True))
def ci_check(path):
    """CI-friendly architecture check. Exit 1 on violations, 0 if clean.

    \b
    Use in GitHub Actions / CI pipelines:

      - name: Architecture check
        run: kodara ci-check

    No config (.kodara/architecture.yml) = exit 0 (not an error).
    """
    from kodara.architecture.guard import check as guard_check

    root = Path(path).resolve()
    kodara_dir = root / ".kodara"
    store = _store(root)
    idx = store.load()

    if not idx:
        _out("kodara: not initialised — skipping architecture check")
        sys.exit(0)

    config_path = kodara_dir / "architecture.yml"
    if not config_path.exists():
        _out("kodara: no architecture.yml — skipping check")
        sys.exit(0)

    try:
        result = guard_check(idx, kodara_dir)
    except ImportError as e:
        _out(f"kodara: {e}")
        sys.exit(1)

    if result.is_clean:
        _out(f"kodara: architecture OK ({result.checked_edges} edges, 0 violations)")
        sys.exit(0)
    else:
        _out(f"kodara: ARCHITECTURE VIOLATION — {len(result.violations)} rule(s) broken")
        for v in result.violations:
            _out(f"  [{v.from_layer}] {Path(v.from_file).name} -> [{v.to_layer}] {Path(v.to_file).name}  ({v.rule})")
        sys.exit(1)


# ── kodara scan ───────────────────────────────────────────────────────────────


@main.command("scan")
@click.argument("url")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.option("--save", is_flag=True, help="Save index to .kodara/ in current directory")
@click.option("--path", "save_path", default=".", show_default=True,
              help="Directory where .kodara/ is written (used with --save)")
def scan(url, as_json, save, save_path):
    """Scan a remote or local repository without indexing your project.

    \b
    Examples:
      kodara scan github.com/tiangolo/fastapi
      kodara scan https://github.com/pallets/flask --json
      kodara scan ./my-other-project --save
    """
    from kodara.remote.scanner import scan_remote, parse_url
    from kodara.indexer.scanner import FreePlanLimitError, FREE_PLAN_LIMIT

    if not as_json:
        _out(f"")
        _out(f"  Scanning {url} ...")
        _out(f"")

    try:
        summary = scan_remote(url)
    except FreePlanLimitError as e:
        if as_json:
            click.echo(json.dumps({"error": "free_plan_limit", "file_count": e.file_count}, indent=2))
        else:
            _out(f"  Free plan limit: project has {e.file_count}+ files (max {FREE_PLAN_LIMIT}).")
            _out(f"  Upgrade at getkodara.dev/pro")
        raise SystemExit(1)
    except ValueError as e:
        if as_json:
            click.echo(json.dumps({"error": str(e)}, indent=2))
        else:
            _out(f"  Error: {e}")
        raise SystemExit(1)

    if as_json:
        click.echo(json.dumps({
            "url": summary.url,
            "file_count": summary.file_count,
            "language_counts": summary.language_counts,
            "top_modules": summary.top_modules,
            "scan_time_s": summary.scan_time_s,
        }, indent=2))
        return

    # ── Human-readable output ────────────────────────────────────────────────
    lang_str = ", ".join(
        f"{lang}({count})"
        for lang, count in sorted(summary.language_counts.items(), key=lambda x: -x[1])
    )
    hub_str = ", ".join(summary.top_modules) if summary.top_modules else "(none detected)"

    _out(f"  Files:     {summary.file_count:,}")
    _out(f"  Languages: {lang_str}")
    _out(f"  Top hubs:  {hub_str}")
    _out(f"  Scan time: {summary.scan_time_s}s")
    _out(f"")

    if save:
        root = Path(save_path).resolve()
        _out(f"  Saving index to {root / '.kodara'} ...")
        _run_init(root, no_summaries=True, full=True, as_json=False, verbose=False)
        _out(f"  Saved.")
        _out(f"")
    else:
        _out(f"  Run with --save to store index in .kodara/")
        _out(f"")


# ── kodara simulate-change ────────────────────────────────────────────────────


@main.command("simulate-change")
@click.argument("file", required=False, default=None)
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--hours", default=2.0, show_default=True, help="Avg dev-hours per affected module")
@click.option("--json", "as_json", is_flag=True)
def simulate_change(file, path, hours, as_json):
    """Simulate the blast radius and cost of changing a file.

    \b
    Example:
      kodara simulate-change auth/session.py
      kodara simulate-change              (auto-selects hub module)
      kodara simulate-change --hours 4 api/routes.py
    """
    from kodara.simulation.engine import simulate

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print("[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    if file is None:
        best = max(
            idx.modules.items(),
            key=lambda kv: len(kv[1].get("dependents", [])),
            default=None,
        )
        if not best:
            _print("[red]No modules indexed.[/red]")
            sys.exit(1)
        file = best[0]
        _print(f"[dim]Auto-selected: {file}[/dim]\n")

    try:
        result = simulate(idx, file, avg_hours=hours)
    except ValueError as e:
        _print(f"[red]{e}[/red]")
        sys.exit(1)

    if as_json:
        import dataclasses
        click.echo(json.dumps(dataclasses.asdict(result), indent=2))
        return

    risk_colors = {"NONE": "green", "LOW": "green", "MEDIUM": "yellow", "HIGH": "red", "CRITICAL": "bold red"}
    color = risk_colors.get(result.risk_level, "white")

    _out(f"")
    _out(f"  Simulate Change -- {result.module_name}")
    _out(f"  ===================================")
    _print(f"  Risk Score:       [bold]{result.risk_score:.0f}[/bold]  ([{color}]{result.risk_level}[/{color}])")
    _out(f"  Direct impact:    {result.direct_count} modules")
    _out(f"  Indirect impact:  {result.indirect_count} modules")
    _out(f"  Downstream cost:  ~{result.downstream_cost_hours:.0f} dev-hours")
    total_affected = result.direct_count + result.indirect_count
    _out(f"  ({total_affected} modules × {hours:.0f}h avg per fix · adjust: --hours N)")

    if result.critical_paths:
        _out(f"")
        _out(f"  Critical paths:")
        for chain in result.critical_paths:
            names = [Path(p).stem for p in chain]
            _out(f"    {' -> '.join(names)}")

    if result.top_affected:
        _out(f"")
        _out(f"  Top affected:")
        _out(f"    {', '.join(result.top_affected)}")

    _out(f"")

    # AI risk narrative via Kodara backend (Pro — requires login)
    from kodara.simulation.engine import simulate_narrative
    narrative = simulate_narrative(result)
    if narrative:
        _out("")
        _out("AI: " + narrative)


# ── kodara heatmap ─────────────────────────────────────────────────────────────


@main.command("heatmap")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--top", default=20, show_default=True, help="Number of top files to show")
@click.option("--json", "as_json", is_flag=True)
def heatmap(path, top, as_json):
    """Show responsibility heatmap -- who owns what and how critical it is.

    \b
    Example:
      kodara heatmap
      kodara heatmap --top 10
      kodara heatmap --json
    """
    from kodara.simulation.heatmap import build_heatmap

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print("[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    entries = build_heatmap(idx)

    if as_json:
        import dataclasses
        click.echo(json.dumps([dataclasses.asdict(e) for e in entries[:top]], indent=2))
        return

    risk_colors = {"LOW": "green", "MEDIUM": "yellow", "HIGH": "red", "CRITICAL": "bold red"}

    _out(f"")
    _out(f"  Responsibility Heatmap -- {idx.display_name}")
    _out(f"  =====================================")

    if not entries:
        _print("  [dim]No modules indexed.[/dim]")
        _out(f"")
        return

    for entry in entries[:top]:
        color = risk_colors.get(entry.risk_level, "white")
        owner_tag = f"@{entry.owner}" if entry.owner != "unknown" else "unknown"
        _print(
            f"  [{color}]{entry.risk_level:<8}[/{color}]  "
            f"{entry.module_name:<30}  "
            f"{owner_tag:<20}  "
            f"deps:{entry.dependents_count:<4}  "
            f"commits:{entry.commit_count}"
        )

    _out(f"")


# ── kodara dna ────────────────────────────────────────────────────────────────


@main.command("dna")
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def dna_cmd(path, as_json):
    """Show the architectural DNA fingerprint of the project."""
    from kodara.dna.fingerprint import build_fingerprint

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _out("kodara: project not initialised. Run 'kodara init' first.")
        raise SystemExit(1)

    dna = build_fingerprint(idx)

    if as_json:
        import dataclasses
        click.echo(json.dumps(dataclasses.asdict(dna), indent=2, ensure_ascii=False))
        return

    _out(f"")
    _out(f"  Project DNA -- {dna.project_name}")
    _out(f"  " + "=" * (14 + len(dna.project_name)))

    # Паттерны по категориям
    has_any = False
    for category, names in dna.detected_patterns.items():
        if names:
            has_any = True
            _out(f"  {category:<12} {', '.join(names)}")
    if not has_any:
        _out(f"  (no known patterns detected)")

    _out(f"")
    if dna.hub_modules:
        _out(f"  Architecture hubs: {', '.join(Path(h).stem for h in dna.hub_modules)}")
    _out(f"  Complexity:  {dna.complexity_score} deps/module")
    if dna.layer_structure:
        _out(f"  Layers:      {', '.join(dna.layer_structure)}")
    if dna.language_profile:
        langs = ", ".join(f"{k}:{v}" for k, v in dna.language_profile.items())
        _out(f"  Languages:   {langs}")
    _out(f"")


# ── kodara learn ──────────────────────────────────────────────────────────────


@main.command("learn")
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def learn_cmd(path, as_json):
    """Save DNA fingerprint to .kodara/dna.json for future comparison."""
    import dataclasses
    from kodara.dna.fingerprint import build_fingerprint

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _out("kodara: project not initialised. Run 'kodara init' first.")
        raise SystemExit(1)

    dna = build_fingerprint(idx)
    dna_dict = dataclasses.asdict(dna)

    kodara_dir = root / ".kodara"
    kodara_dir.mkdir(parents=True, exist_ok=True)
    dna_path = kodara_dir / "dna.json"

    tmp = dna_path.with_suffix(".tmp")
    tmp.write_text(json.dumps(dna_dict, indent=2, ensure_ascii=False), encoding="utf-8")
    tmp.replace(dna_path)

    if as_json:
        click.echo(json.dumps({"saved": str(dna_path), "dna": dna_dict}, indent=2, ensure_ascii=False))
        return

    _out(f"")
    _out(f"  DNA saved -- {dna.project_name}")
    _out(f"  Path: {dna_path}")
    _out(f"")

    all_detected = [name for names in dna.detected_patterns.values() for name in names]
    if all_detected:
        _out(f"  Detected patterns: {', '.join(all_detected)}")
    else:
        _out(f"  No known patterns detected yet.")
    if dna.hub_modules:
        _out(f"  Hub modules: {', '.join(Path(h).stem for h in dna.hub_modules)}")
    _out(f"  Complexity:  {dna.complexity_score} deps/module")
    _out(f"")


# ── kodara evolve ─────────────────────────────────────────────────────────────


@main.command("evolve")
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def evolve_cmd(path, as_json):
    """Show how the project DNA changed over time (via snapshots)."""
    from kodara.dna.evolution import get_evolution

    root = Path(path).resolve()
    diffs = get_evolution(root)

    if not diffs:
        if as_json:
            click.echo(json.dumps([], indent=2))
        else:
            _out(f"")
            _out(f"  No snapshots yet. Run 'kodara snapshot' first.")
            _out(f"")
        return

    if as_json:
        import dataclasses
        click.echo(json.dumps([dataclasses.asdict(d) for d in diffs], indent=2, ensure_ascii=False))
        return

    _out(f"")
    _out(f"  DNA Evolution -- {root.name}")
    _out(f"  " + "=" * (16 + len(root.name)))

    for diff in diffs:
        from datetime import datetime
        ts = datetime.fromtimestamp(diff.timestamp).strftime("%Y-%m-%d %H:%M") if diff.timestamp else diff.snapshot_name[:16]
        _out(f"")
        _out(f"  Snapshot: {diff.snapshot_name}  ({ts})")
        if diff.added_patterns:
            _out(f"    + Added:    {', '.join(diff.added_patterns)}")
        if diff.removed_patterns:
            _out(f"    - Removed:  {', '.join(diff.removed_patterns)}")
        delta_sign = "+" if diff.complexity_delta >= 0 else ""
        _out(f"    Complexity: {delta_sign}{diff.complexity_delta:+.2f}")
        fc_sign = "+" if diff.file_count_delta >= 0 else ""
        _out(f"    Files:      {fc_sign}{diff.file_count_delta:+d}")

    _out(f"")


# ── kodara compare ────────────────────────────────────────────────────────────


@main.command("compare")
@click.argument("path1", type=click.Path(exists=True))
@click.argument("path2", type=click.Path(exists=True))
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def compare_cmd(path1, path2, as_json):
    """Compare the DNA fingerprints of two projects.

    \b
    Both projects must have a .kodara/ index (run 'kodara init' in each).

    Example:
      kodara compare ./my-api ./my-frontend
    """
    from kodara.dna.fingerprint import build_fingerprint

    root1 = Path(path1).resolve()
    root2 = Path(path2).resolve()

    store1 = _store(root1)
    store2 = _store(root2)

    idx1 = store1.load()
    idx2 = store2.load()

    if not idx1:
        _out(f"kodara: '{root1.name}' is not initialised. Run 'kodara init {path1}' first.")
        raise SystemExit(1)
    if not idx2:
        _out(f"kodara: '{root2.name}' is not initialised. Run 'kodara init {path2}' first.")
        raise SystemExit(1)

    dna1 = build_fingerprint(idx1)
    dna2 = build_fingerprint(idx2)

    patterns1: set[str] = {n for names in dna1.detected_patterns.values() for n in names}
    patterns2: set[str] = {n for names in dna2.detected_patterns.values() for n in names}

    common = sorted(patterns1 & patterns2)
    only1 = sorted(patterns1 - patterns2)
    only2 = sorted(patterns2 - patterns1)

    if as_json:
        click.echo(json.dumps({
            "project1": dna1.project_name,
            "project2": dna2.project_name,
            "common_patterns": common,
            f"only_in_{dna1.project_name}": only1,
            f"only_in_{dna2.project_name}": only2,
            "complexity_1": dna1.complexity_score,
            "complexity_2": dna2.complexity_score,
            "layers_1": dna1.layer_structure,
            "layers_2": dna2.layer_structure,
        }, indent=2, ensure_ascii=False))
        return

    _out(f"")
    _out(f"  DNA Compare")
    _out(f"  ===========")
    _out(f"  {dna1.project_name}  vs  {dna2.project_name}")
    _out(f"")

    if common:
        _out(f"  Common patterns:      {', '.join(common)}")
    else:
        _out(f"  Common patterns:      (none)")

    if only1:
        _out(f"  Only in {dna1.project_name:<12} {', '.join(only1)}")
    if only2:
        _out(f"  Only in {dna2.project_name:<12} {', '.join(only2)}")

    _out(f"")
    _out(f"  Complexity:  {dna1.project_name}={dna1.complexity_score}  {dna2.project_name}={dna2.complexity_score}")

    layers1 = ", ".join(dna1.layer_structure) or "(none)"
    layers2 = ", ".join(dna2.layer_structure) or "(none)"
    _out(f"  Layers [{dna1.project_name}]:  {layers1}")
    _out(f"  Layers [{dna2.project_name}]:  {layers2}")
    _out(f"")


# ── kodara why ────────────────────────────────────────────────────────────────


@main.command()
@click.argument("file", required=False, default=None)
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def why(file, path, as_json):
    """Show WHY a file exists — git history + notes + summary.

    \b
    Examples:
      kodara why src/auth.py
      kodara why              (auto-selects the hub module)
    """
    from kodara.intelligence.why import get_why
    from kodara.notes.store import NotesStore

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print("[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    if file is None:
        # Auto-select hub module (most dependents)
        best = max(
            idx.modules.items(),
            key=lambda kv: len(kv[1].get("dependents", [])),
            default=None,
        )
        if not best:
            _print("[red]No modules indexed.[/red]")
            sys.exit(1)
        file = best[0]
        _print(f"[dim]Auto-selected: {file}[/dim]\n")

    notes_store = NotesStore(store.kodara_dir)
    result = get_why(idx, file, notes_store=notes_store)

    if as_json:
        import dataclasses
        click.echo(json.dumps(dataclasses.asdict(result), indent=2))
        return

    _out(f"\nWhy: {result.module_name}")
    _out(f"File: {result.file}")
    _out("")

    if result.summary:
        summary_text = result.summary[:150] + "..." if len(result.summary) > 150 else result.summary
        _out(f"  Summary:     {summary_text}")

    if result.git_commits:
        _out(f"  Git commits: {result.git_commits}")
    if result.git_reason:
        _out(f"  Last change: {result.git_reason}")
    if result.git_authors:
        authors_str = ", ".join(
            f"{a['name']} ({a['commits']})" for a in result.git_authors[:3]
        )
        _out(f"  Authors:     {authors_str}")
    elif not result.git_commits:
        _out(f"  Git history: not available (run in a git repo with commits)")

    if result.notes:
        _out("")
        _out("  Notes:")
        for note_text in result.notes[:5]:
            _out(f"    - {note_text}")

    _out("")
    _out(f"  Dependents:  {result.dependents_count} module(s) import this file")
    if result.dependents:
        for dep in result.dependents[:5]:
            _out(f"    <- {Path(dep).stem}")
    _out("")

    # AI explanation via Kodara backend (Pro — requires login)
    from kodara.intelligence.why import get_why_ai
    narrative = get_why_ai(idx, file)
    if narrative:
        _out("AI: " + narrative)
    else:
        _out("  Tip: run `kodara login` to get AI analysis of this file")



# ── kodara bus-factor ─────────────────────────────────────────────────────────


@main.command("bus-factor")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.option("--top", default=10, show_default=True, help="Show top N risks")
def bus_factor(path, as_json, top):
    """Show files with single-author bus factor risk.

    \b
    Files where one person is the sole contributor AND others depend on them.
    If no git history — reports gracefully.
    """
    from kodara.intelligence.bus_factor import get_bus_factor

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print("[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    risks = get_bus_factor(idx)

    if as_json:
        import dataclasses
        click.echo(json.dumps([dataclasses.asdict(r) for r in risks[:top]], indent=2))
        return

    if not risks:
        # Check if there's any git data at all
        has_git = any(
            mod.get("git", {}).get("top_authors")
            for mod in idx.modules.values()
        )
        if not has_git:
            _out("\n  No git history found.")
            _out("  Run: kodara init  (in a git repository with commits)")
            _out("")
        else:
            _out("\n  No bus factor risks found.")
            _out("  All impactful files have multiple contributors.")
            _out("")
        return

    risk_icons = {"CRITICAL": "!!", "HIGH": " !", "MEDIUM": " ~", "LOW": "  "}

    _out(f"\nBus Factor Risks — {root.name}")
    _out(f"{'=' * (21 + len(root.name))}")
    _out("")

    for r in risks[:top]:
        icon = risk_icons.get(r.risk_level, "  ")
        hub_tag = " [hub]" if r.is_hub else ""
        _out(f"  {icon} {r.risk_level:8}  {r.module_name}{hub_tag}")
        _out(f"          Author:     {r.sole_author}  ({r.commit_count} commits)")
        _out(f"          Dependents: {r.dependents_count}")
        _out(f"          File:       {r.file}")
        _out("")

    if len(risks) > top:
        _out(f"  ... and {len(risks) - top} more (use --top to see more)")
        _out("")


# ── kodara debt ───────────────────────────────────────────────────────────────


@main.command()
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.option("--top", default=15, show_default=True, help="Show top N files")
def debt(path, as_json, top):
    """Show technical debt heatmap — files that change often AND break many things.

    \b
    Formula: debt_score = commit_count * dependents_count
    High score = high churn + high blast radius = most dangerous area.
    """
    from kodara.intelligence.debt import get_debt

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _print("[red]Not initialised.[/red] Run: kodara init")
        sys.exit(1)

    items = get_debt(idx, top_n=top)

    if as_json:
        import dataclasses
        click.echo(json.dumps([dataclasses.asdict(d) for d in items], indent=2))
        return

    if not items or items[0].debt_score == 0:
        _out("\n  No technical debt signals found.")
        _out("  Either no git history or no cross-file dependencies.")
        _out("")
        return

    risk_icons_text = {
        "CRITICAL": "!!",
        "HIGH": " !",
        "MEDIUM": " ~",
        "LOW": "  ",
        "NONE": "  ",
    }

    _out(f"\nTechnical Debt Heatmap — {root.name}")
    _out(f"{'=' * (25 + len(root.name))}")
    _out("")
    _out(f"  {'RISK':8}  {'SCORE':>6}  {'COMMITS':>7}  {'DEPS':>4}  FILE")
    _out(f"  {'-'*8}  {'-'*6}  {'-'*7}  {'-'*4}  {'-'*30}")

    shown = 0
    for d in items:
        if d.debt_score == 0:
            break
        icon = risk_icons_text.get(d.risk_level, "  ")
        _out(
            f"  {icon} {d.risk_level:6}  {d.debt_score:>6}  "
            f"{d.commit_count:>7}  {d.dependents_count:>4}  {d.module_name}"
        )
        shown += 1

    _out("")
    if shown == 0:
        _out("  All files have debt_score = 0 (no commits or no dependents).")
        _out("")


def _setup_mcp(project_root: str) -> None:
    """Auto-write Kodara MCP config into Claude Desktop, Cursor and VS Code."""
    import json as _json
    import os
    import sys
    from pathlib import Path as _Path

    project_abs = str(_Path(project_root).resolve())

    entry = {
        "command": "kodara",
        "args": ["mcp", "--path", project_abs],
    }

    # Detect config paths per OS
    configs = []
    if sys.platform == "win32":
        appdata = os.environ.get("APPDATA", "")
        configs = [
            ("Claude Desktop", _Path(appdata) / "Claude" / "claude_desktop_config.json"),
            ("Cursor",         _Path(appdata) / "Cursor" / "User" / "mcp.json"),
            ("VS Code",        _Path(appdata) / "Code" / "User" / "mcp.json"),
        ]
    elif sys.platform == "darwin":
        configs = [
            ("Claude Desktop", _Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"),
            ("Cursor",         _Path.home() / "Library" / "Application Support" / "Cursor" / "User" / "mcp.json"),
            ("VS Code",        _Path.home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json"),
        ]
    else:
        configs = [
            ("Claude Desktop", _Path.home() / ".config" / "Claude" / "claude_desktop_config.json"),
            ("Cursor",         _Path.home() / ".config" / "Cursor" / "User" / "mcp.json"),
            ("VS Code",        _Path.home() / ".config" / "Code" / "User" / "mcp.json"),
        ]

    found_any = False

    for tool_name, cfg_path in configs:
        if not cfg_path.parent.exists():
            continue  # tool not installed

        # Read existing config or start fresh
        existing = {}
        if cfg_path.exists():
            try:
                existing = _json.loads(cfg_path.read_text(encoding="utf-8"))
            except Exception:
                existing = {}

        # Inject/update kodara entry
        if "mcpServers" not in existing:
            existing["mcpServers"] = {}
        existing["mcpServers"]["kodara"] = entry

        # Write back atomically
        cfg_path.parent.mkdir(parents=True, exist_ok=True)
        cfg_path.write_text(_json.dumps(existing, indent=2), encoding="utf-8")

        _out(f"  OK  {tool_name}")
        _out(f"      {cfg_path}")
        found_any = True

    if not found_any:
        _out("")
        _out("  No Claude Desktop, Cursor or VS Code installation found.")
        _out("")
        _out("  Add manually to your MCP config:")
        _out("  {")
        _out("    \"mcpServers\": {")
        _out("      \"kodara\": {")
        _out(f"        \"command\": \"kodara\",")
        _out(f"        \"args\": [\"mcp\", \"--path\", \"{project_abs}\"]")
        _out("      }")
        _out("    }")
        _out("  }")
    else:
        _out("")
        _out("  Restart Claude Desktop / Cursor / VS Code to activate.")
        _out("  Kodara tools available: kodara_ask, kodara_impact, kodara_context, kodara_search, kodara_status")


@main.command("mcp")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
@click.option("--setup", "do_setup", is_flag=True, help="Auto-configure MCP in Claude Desktop and Cursor")
def mcp_server(path, do_setup):
    """Start Kodara MCP server (Pro feature).

    \b
    Use --setup to auto-configure Claude Desktop and Cursor:
      kodara mcp --setup --path /path/to/project

    Or add manually to your MCP config:
      {
        "mcpServers": {
          "kodara": {
            "command": "kodara",
            "args": ["mcp", "--path", "/path/to/project"]
          }
        }
      }

    Requires: pip install kodara[pro]
    """
    root = Path(path).resolve()

    if do_setup:
        _out("")
        _out("  Kodara MCP Setup")
        _out("  ================")
        _setup_mcp(str(root))
        _out("")
        return

    try:
        from kodara.mcp.server import run_server
    except ImportError:
        _out("  Kodara MCP server requires: pip install kodara[pro]")
        sys.exit(1)

    import asyncio
    asyncio.run(run_server(str(root)))


# ── kodara repos ──────────────────────────────────────────────────────────────


@main.group()
def repos():
    """Manage known Kodara-indexed projects on this machine."""
    pass


@repos.command("list")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def repos_list(as_json):
    """Show all known indexed projects."""
    from kodara.repos.manager import list_repos

    entries = list_repos()

    if as_json:
        import json as _json
        click.echo(_json.dumps([
            {
                "path": e.path,
                "display_name": e.display_name,
                "file_count": e.file_count,
                "indexed_at": e.indexed_at,
                "exists": e.exists,
            }
            for e in entries
        ], indent=2))
        return

    if not entries:
        _out("  No indexed projects found.")
        _out("  Run: kodara init <path>")
        return

    _out("")
    _out("  Known Projects")
    _out("  ==============")
    for e in entries:
        import datetime
        if e.indexed_at:
            date_str = datetime.datetime.fromtimestamp(e.indexed_at).strftime("%Y-%m-%d")
        else:
            date_str = "-"

        if e.exists:
            files_str = f"files:{e.file_count}"
            _out(f"  {e.display_name:<20} {e.path:<50} {files_str:<12} {date_str}")
        else:
            _out(f"  [not found]          {e.path:<50} {'-':<12} {date_str}")
    _out("")


@repos.command("clean")
def repos_clean():
    """Remove stale (no longer existing) projects from the registry."""
    from kodara.repos.manager import clean_repos

    removed = clean_repos()

    if not removed:
        _out("  Nothing to clean — all registered projects still exist.")
        return

    _out(f"  Removed {len(removed)} stale {'entry' if len(removed) == 1 else 'entries'}:")
    for p in removed:
        _out(f"    {p}")
    _out("")


# ── kodara init-ci ────────────────────────────────────────────────────────────


@main.command("init-ci")
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--force", is_flag=True, help="Overwrite existing workflow file")
def init_ci(path, force):
    """Create .github/workflows/kodara.yml for GitHub Actions CI.

    \b
    On every push/PR GitHub will:
      1. pip install kodara
      2. kodara init --no-summaries
      3. kodara ci-check
    """
    from kodara.ci.generator import generate_workflow, workflow_exists

    root = Path(path).resolve()

    if workflow_exists(root) and not force:
        rel = Path(".github") / "workflows" / "kodara.yml"
        _out(f"CI workflow already exists: {rel}")
        _out("Use --force to overwrite.")
        return

    if workflow_exists(root) and force:
        target = root / ".github" / "workflows" / "kodara.yml"
        target.unlink()

    try:
        created = generate_workflow(root)
        try:
            rel = created.relative_to(root)
        except ValueError:
            rel = created
        _out(f"CI workflow created: {rel}")
        _out("")
        _out("On every push/PR GitHub will:")
        _out("  1. pip install kodara")
        _out("  2. kodara init --no-summaries")
        _out("  3. kodara ci-check")
        _out("")
        _out("Next steps:")
        _out("  1. kodara arch-init          (define your layer rules)")
        _out("  2. git add .github/workflows/kodara.yml .kodara/architecture.yml")
        _out('  3. git commit -m "Add Kodara architecture check"')
    except FileExistsError as exc:
        _out(f"CI workflow already exists: {exc}")
        _out("Use --force to overwrite.")


# ── kodara login ──────────────────────────────────────────────────────────────


@main.command()
@click.option("--email", default=None, help="Email address to log in with")
def login(email):
    """Log in to Kodara Cloud via email OTP (magic link)."""
    from kodara.cloud.auth import request_otp, verify_otp

    if not email:
        email = click.prompt("Enter your email")

    email = email.strip()

    try:
        request_otp(email)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Failed to send OTP: {exc}")
        raise SystemExit(1)

    _out("Check your email for a 6-digit code.")
    code = click.prompt("Enter code").strip()

    try:
        user = verify_otp(email, code)
    except Exception as exc:
        _out(f"Login failed: {exc}")
        raise SystemExit(1)

    _out(f"Logged in as {user.get('email', email)}")


# ── kodara logout ─────────────────────────────────────────────────────────────


@main.command()
def logout():
    """Log out from Kodara Cloud and clear local session."""
    from kodara.cloud.auth import logout as _cloud_logout

    _cloud_logout()
    _out("Logged out.")


# ── kodara whoami ─────────────────────────────────────────────────────────────


@main.command()
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def whoami(as_json):
    """Show the currently logged-in user."""
    from kodara.cloud.auth import get_current_user

    user = get_current_user()

    if user is None:
        if as_json:
            _out(json.dumps({"logged_in": False}))
        else:
            _out("Not logged in. Run: kodara login")
        return

    if as_json:
        _out(json.dumps({"logged_in": True, **user}))
    else:
        _out(f"Logged in as {user.get('email', '?')}")


# ── kodara backup ─────────────────────────────────────────────────────────────


@main.command("backup")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
def cloud_backup(path):
    """Upload current index to Kodara Cloud.

    \b
    Requires: kodara login
    Run 'kodara init' first if the project is not yet indexed.
    """
    from kodara.cloud.client import is_authenticated
    from kodara.cloud.backup import backup as _backup

    if not is_authenticated():
        click.echo("Pro feature. Login first: kodara login")
        click.echo("Get Pro at getkodara.dev")
        return

    root = Path(path).resolve()
    store = _store(root)
    idx = store.load()

    if not idx:
        _out("No index found. Run: kodara init")
        raise SystemExit(1)

    from kodara.cloud.client import load_session
    session = load_session() or {}
    user_id = session.get("user_id") or session.get("id") or session.get("email", "")
    if not user_id:
        _out("Not logged in. Run: kodara login")
        raise SystemExit(1)

    try:
        backup_id = _backup(idx, user_id)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Backup failed: {exc}")
        raise SystemExit(1)

    _out(f"Backed up: {idx.display_name} ({idx.file_count} files) -> cloud")
    _out(f"Backup ID: {backup_id}")


# ── kodara restore ────────────────────────────────────────────────────────────


@main.command("restore")
@click.argument("project_name")
@click.option("--path", default=".", type=click.Path(exists=True), help="Destination directory")
def cloud_restore(project_name, path):
    """Download the latest cloud backup and save to .kodara/.

    \b
    Requires: kodara login
    Example:
      kodara restore my-project
      kodara restore my-project --path /other/dir
    """
    from kodara.cloud.client import is_authenticated
    from kodara.cloud.backup import restore as _restore

    if not is_authenticated():
        click.echo("Pro feature. Login first: kodara login")
        click.echo("Get Pro at getkodara.dev")
        return

    root = Path(path).resolve()
    _out(f"Restoring {project_name}...")

    try:
        idx = _restore(project_name, root)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except ValueError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Restore failed: {exc}")
        raise SystemExit(1)

    store = _store(root)
    _out(f"Restored: {idx.file_count} files -> {store.kodara_dir}")


# ── kodara backups ────────────────────────────────────────────────────────────


@main.command("backups")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
def cloud_backups(as_json):
    """List all projects backed up in Kodara Cloud.

    \b
    Requires: kodara login
    """
    from kodara.cloud.client import is_authenticated, load_session
    from kodara.cloud.backup import list_backups as _list_backups

    if not is_authenticated():
        click.echo("Pro feature. Login first: kodara login")
        click.echo("Get Pro at getkodara.dev")
        return

    session = load_session() or {}
    user_id = session.get("user_id") or session.get("id") or session.get("email", "")
    if not user_id:
        _out("Not logged in. Run: kodara login")
        raise SystemExit(1)

    try:
        items = _list_backups(user_id)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Failed to fetch backups: {exc}")
        raise SystemExit(1)

    if as_json:
        import dataclasses
        click.echo(json.dumps([dataclasses.asdict(b) for b in items], indent=2))
        return

    if not items:
        _out("No cloud backups found.")
        _out("Run: kodara backup")
        return

    _out("")
    _out("Cloud Backups")
    _out("=============")
    for b in items:
        # Format created_at: keep first 16 chars (YYYY-MM-DDTHH:MM)
        created = b.created_at[:16].replace("T", " ") if b.created_at else "-"
        _out(f"  {b.project_name:<20}  {b.file_count} files    {created}")
    _out("")


# ── kodara team cloud-* ───────────────────────────────────────────────────────


@team.command("cloud-create")
@click.argument("name")
def team_cloud_create(name):
    """Create a team in Kodara Cloud.

    \b
    Requires: kodara login
    Example:  kodara team cloud-create my-team
    """
    from kodara.cloud.client import is_authenticated, load_session
    from kodara.cloud.team_cloud import create_team

    if not is_authenticated():
        click.echo("Team feature. Login first: kodara login")
        click.echo("Get Team plan at getkodara.dev")
        return

    session = load_session() or {}
    user_id = session.get("user_id") or session.get("id") or session.get("sub") or session.get("email", "")
    if not user_id:
        _out("Not logged in. Run: kodara login")
        raise SystemExit(1)

    try:
        info = create_team(name, user_id)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Cloud error: {exc}")
        raise SystemExit(1)

    _out(f"Team created: {info.name}")
    _out(f"Invite code: {info.invite_code}")
    _out(f"Share this code with teammates: kodara team cloud-join {info.invite_code}")


@team.command("cloud-join")
@click.argument("invite_code")
def team_cloud_join(invite_code):
    """Join an existing team in Kodara Cloud.

    \b
    Requires: kodara login
    Example:  kodara team cloud-join a1b2c3d4
    """
    from kodara.cloud.client import is_authenticated, load_session
    from kodara.cloud.team_cloud import join_team

    if not is_authenticated():
        click.echo("Team feature. Login first: kodara login")
        click.echo("Get Team plan at getkodara.dev")
        return

    session = load_session() or {}
    user_id = session.get("user_id") or session.get("id") or session.get("sub") or session.get("email", "")
    if not user_id:
        _out("Not logged in. Run: kodara login")
        raise SystemExit(1)

    try:
        info = join_team(invite_code, user_id)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except ValueError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Cloud error: {exc}")
        raise SystemExit(1)

    _out(f"Joined team: {info.name}")


@team.command("cloud-push")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
def team_cloud_push(path):
    """Push current index to team memory in Kodara Cloud.

    \b
    Requires: kodara login  +  kodara team cloud-create / cloud-join
    Example:  kodara team cloud-push
    """
    from kodara.cloud.client import is_authenticated, load_session
    from kodara.cloud.team_cloud import push_to_team, get_saved_team_id

    if not is_authenticated():
        click.echo("Team feature. Login first: kodara login")
        click.echo("Get Team plan at getkodara.dev")
        return

    session = load_session() or {}
    user_id = session.get("user_id") or session.get("id") or session.get("sub") or session.get("email", "")
    if not user_id:
        _out("Not logged in. Run: kodara login")
        raise SystemExit(1)

    team_id = get_saved_team_id()
    if not team_id:
        _out("No team configured. Run: kodara team cloud-create <name>  or  kodara team cloud-join <code>")
        raise SystemExit(1)

    root = Path(path).resolve()
    store = _store(root)
    if not store.exists():
        _out("Project not indexed. Run: kodara init")
        raise SystemExit(1)

    idx = store.load()
    if not idx:
        _out("Index is empty. Run: kodara init")
        raise SystemExit(1)

    try:
        push_to_team(team_id, idx, user_id)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Cloud push failed: {exc}")
        raise SystemExit(1)

    _out(f"Pushed to team: {idx.display_name} ({idx.file_count} files)")


@team.command("cloud-pull")
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root")
def team_cloud_pull(path):
    """Pull team memory from Kodara Cloud into .kodara/.

    \b
    Requires: kodara login  +  kodara team cloud-create / cloud-join
    Example:  kodara team cloud-pull
    """
    from kodara.cloud.client import is_authenticated
    from kodara.cloud.team_cloud import pull_from_team, get_saved_team_id

    if not is_authenticated():
        click.echo("Team feature. Login first: kodara login")
        click.echo("Get Team plan at getkodara.dev")
        return

    team_id = get_saved_team_id()
    if not team_id:
        _out("No team configured. Run: kodara team cloud-create <name>  or  kodara team cloud-join <code>")
        raise SystemExit(1)

    root = Path(path).resolve()

    try:
        idx = pull_from_team(team_id, root)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except ValueError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Cloud pull failed: {exc}")
        raise SystemExit(1)

    _out(f"Pulled from team: {idx.display_name} ({idx.file_count} files)")


@team.command("cloud-status")
def team_cloud_status():
    """Show team cloud membership and sync status.

    \b
    Requires: kodara login
    """
    from kodara.cloud.client import is_authenticated, load_session
    from kodara.cloud.team_cloud import get_team_info, get_saved_team_id

    if not is_authenticated():
        click.echo("Team feature. Login first: kodara login")
        click.echo("Get Team plan at getkodara.dev")
        return

    session = load_session() or {}
    user_id = session.get("user_id") or session.get("id") or session.get("sub") or session.get("email", "")
    if not user_id:
        _out("Not logged in. Run: kodara login")
        raise SystemExit(1)

    saved_team_id = get_saved_team_id()

    try:
        teams = get_team_info(user_id)
    except ImportError as exc:
        _out(str(exc))
        raise SystemExit(1)
    except Exception as exc:
        _out(f"Cloud error: {exc}")
        raise SystemExit(1)

    if not teams:
        _out("No cloud teams found.")
        _out("Create one: kodara team cloud-create <name>")
        _out("Or join:   kodara team cloud-join <invite-code>")
        return

    for t in teams:
        active = " (active)" if saved_team_id and t.id == saved_team_id else ""
        _out(f"Team: {t.name}{active}")
        _out(f"Members: {t.member_count}")
        _out(f"Invite code: {t.invite_code}")
        _out("")


@main.command("clone-pattern")
@click.argument("url")
@click.option("--into", "target", required=True, help="Target directory path")
def clone_pattern_cmd(url: str, target: str) -> None:
    """Clone architectural pattern from a repo into a new directory."""
    from kodara.clone.generator import clone_pattern
    click.echo(f"Scanning {url} ...")
    result = clone_pattern(url, target)
    if result.error:
        click.echo(f"Error: {result.error}")
        return
    click.echo(f"Patterns applied: {', '.join(result.patterns_applied) or 'general'}")
    click.echo(f"Dirs created:  {len(result.dirs_created)}")
    click.echo(f"Files created: {len(result.files_created)}")
    click.echo(f"Done: {result.target}")


# ── kodara drift ──────────────────────────────────────────────────────────────


@main.command("drift")
@click.argument("path", default=".", type=click.Path(exists=True))
def drift_cmd(path: str) -> None:
    """Check for architectural drift since last snapshot."""
    from kodara.drift.detector import get_drift_report
    report = get_drift_report(path)
    if not report.has_alerts:
        click.echo("No significant drift detected.")
        return
    click.echo(f"Drift Report ({len(report.alerts)} alerts)")
    click.echo("-" * 40)
    for alert in report.alerts:
        prefix = "[HIGH]" if alert.level == "high" else "[MED] " if alert.level == "medium" else "[INFO]"
        click.echo(f"{prefix} {alert.message}")
        if alert.detail:
            click.echo(f"       {alert.detail}")
    if report.new_files:
        click.echo(f"\nNew files: +{report.new_files}")
    if report.new_hubs:
        click.echo(f"New hubs:  {', '.join(report.new_hubs[:5])}")


# ── Shared helpers ────────────────────────────────────────────────────────────


def _store(root: Path):
    from kodara.memory.store import MemoryStore
    return MemoryStore(root)


def _run_init(root: Path, *, no_summaries: bool, full: bool, as_json: bool, verbose: bool) -> None:
    """Core indexing logic — used by both init and watch."""
    from kodara.indexer.scanner import RepositoryScanner, FreePlanLimitError, FREE_PLAN_LIMIT
    from kodara.graph.dependency import DependencyGraph
    from kodara.memory.store import MemoryStore, KodaraIndex
    from kodara.summarizer import generate_summaries
    from kodara.git.history import is_git_repo, collect_file_histories, get_repo_stats

    store = MemoryStore(root)
    existing = store.load() if not full else None

    t0 = time.time()

    if verbose:
        _out(f"\n  Kodara — Initialising {root.name}")

    # Scan
    scanner = RepositoryScanner(root)
    try:
        scan = scanner.scan()
    except FreePlanLimitError as e:
        _print(f"\n[bold yellow]Free Plan Limit[/bold yellow]")
        _print(f"  This project has [bold]{e.file_count}+[/bold] files.")
        _print(f"  Free plan supports up to [bold]{FREE_PLAN_LIMIT} files[/bold].")
        _print(f"")
        _print(f"  Upgrade to [bold]Kodara Pro[/bold] for unlimited projects:")
        _print(f"  [dim]getkodara.dev/pro[/dim]")
        _print(f"")
        _print(f"  ROI: Pro costs $19/month. Average dev saves $638/month. [bold]33x return.[/bold]")
        raise SystemExit(1)

    # Graph
    graph = DependencyGraph()
    graph.build(scan)
    graph_data = graph.to_dict()

    # Git
    git_file_info: dict = {}
    git_stats: dict = {}
    if is_git_repo(str(root)):
        git_file_info = collect_file_histories(str(root))
        git_stats = get_repo_stats(str(root))

    # Modules
    modules: dict[str, dict] = {}
    reused = 0
    for file_path, info in scan.files.items():
        cached_summary = ""
        if existing and file_path in existing.modules:
            ex = existing.modules[file_path]
            if ex.get("file_hash") == info.hash:
                cached_summary = ex.get("summary", "")
                reused += 1

        modules[file_path] = {
            "path": file_path,
            "module_name": graph.node_data(file_path).get("module_name", ""),
            "language": info.language,
            "classes": info.classes,
            "functions": info.functions,
            "exports": info.exports,
            "imports": info.imports,
            "file_hash": info.hash,
            "summary": cached_summary,
            "dependencies": graph.get_dependencies(file_path),
            "dependents": graph.get_dependents(file_path),
            "git": git_file_info.get(str((root / file_path).resolve()), git_file_info.get(file_path, {})),
        }

    # Summaries
    if not no_summaries:
        needs = {p: m for p, m in modules.items() if not m["summary"]}
        if needs:
            try:
                sums = generate_summaries(needs)
                for p, s in sums.items():
                    modules[p]["summary"] = s
            except Exception:
                pass

    # Save
    repo_id = MemoryStore.repo_id_from_path(str(root))
    idx = KodaraIndex(
        repo_id=repo_id,
        display_name=root.name,
        source_root=str(root),
        indexed_at=time.time(),
        index_version=2,
        file_count=len(scan.files),
        language_counts=scan.languages,
        modules=modules,
        graph_edges=graph_data["edges"],
        file_hashes={p: f.hash for p, f in scan.files.items()},
    )
    store.save(idx)

    # Auto-snapshot once per day
    try:
        from kodara.snapshots.store import SnapshotStore
        snap_store = SnapshotStore(store.kodara_dir)
        snap_store.auto_snapshot(idx)
    except Exception:
        pass

    # Register in global repos registry
    try:
        from kodara.repos.manager import register_repo
        register_repo(str(root))
    except Exception:
        pass  # never break init due to registry failure

    elapsed = time.time() - t0

    if as_json:
        click.echo(json.dumps({
            "success": True,
            "file_count": len(scan.files),
            "languages": scan.languages,
            "edge_count": len(graph_data["edges"]),
            "elapsed": round(elapsed, 2),
            "git": git_stats,
        }, indent=2))
    elif verbose:
        _out(f"")
        _out(f"  Files:    {len(scan.files)}")
        _out(f"  Languages: {dict(scan.languages)}")
        _out(f"  Edges:    {len(graph_data['edges'])}")
        if reused:
            _out(f"  Cached:   {reused} summaries reused")
        if git_stats.get("branch"):
            _out(f"  Git:      {git_stats['branch']} - {git_stats.get('total_commits',0)} commits")
        _out(f"  Memory:   {store.kodara_dir}")
        _out(f"  Done in:  {elapsed:.1f}s")
        _out(f"")
        _quick_win_block(idx)


def _quick_win_block(idx, verbose: bool = True) -> None:
    """Print Quick Win block after successful init."""
    if not verbose:
        return
    file_count = len(idx.modules)
    if file_count == 0:
        return

    raw_tokens    = file_count * 800
    kodara_tokens = min(8, file_count) * 250
    savings_pct   = min(99, int((raw_tokens - kodara_tokens) / raw_tokens * 100))
    cost_raw      = raw_tokens    / 1_000_000 * 3
    cost_kodara   = kodara_tokens / 1_000_000 * 3

    # Find hub file (most dependents)
    hub_entry = max(
        idx.modules.items(),
        key=lambda kv: len(kv[1].get("dependents", [])),
        default=None,
    )
    hub_name = Path(hub_entry[0]).name if hub_entry else None
    hub_deps = len(hub_entry[1].get("dependents", [])) if hub_entry else 0

    _out("--------------------------------------")
    _out(f" Kodara ready. {file_count} files indexed.")
    _out("")
    _out(" Per AI session estimate:")
    _out(f"   Without Kodara: ~{raw_tokens:,} tokens  (~${cost_raw:.2f})")
    _out(f"   With Kodara:    ~{kodara_tokens:,} tokens  (~${cost_kodara:.2f})")
    _out(f"   Savings:        ~{savings_pct}% fewer tokens")
    _out("")
    if hub_name and hub_deps > 0:
        _out(f" Your codebase at a glance:")
        _out(f"   Hub file:  {hub_name}  ({hub_deps} modules depend on it)")
        _out("")
    _out(" Try now:")
    if hub_name:
        _out(f"   kodara simulate-change {hub_name}   -- blast radius of your main hub")
    _out(f"   kodara ask \"how does this project work?\"")
    if hub_name:
        _out(f"   kodara why {hub_name}               -- why this file exists")
    _out("--------------------------------------")


def _regenerate_context(root: Path, verbose: bool = False) -> None:
    from kodara.injector.inject import generate_context
    try:
        out = generate_context(str(root))
        if verbose:
            rel = Path(out).relative_to(root)
            _print(f"\n[bold green]OK[/bold green] {rel}\n")
        else:
            _print("  [dim]↳ ARCHITECTURE.md updated[/dim]")
    except ValueError as e:
        _print(f"[red]{e}[/red]")
