"""Analysis, embedding, ideas, gaps, refs, trends, and related commands."""

from __future__ import annotations

from pathlib import Path

import click
from rich.table import Table

from .common import console, pass_cfg_db, _get_config
from ..config import Config
from ..db import Database


def register(main):
    """Register all analysis commands with the main CLI group."""
    main.add_command(analyze)
    main.add_command(ask)
    main.add_command(compare)
    main.add_command(embed)
    main.add_command(embed_ideas)
    main.add_command(similar)
    main.add_command(clusters)
    main.add_command(ideas)
    main.add_command(dedup_ideas)
    main.add_command(gaps)
    main.add_command(refs)
    main.add_command(trends)
    main.add_command(status)
    main.add_command(revisions)
    main.add_command(idea_overlap)
    main.add_command(co_occurrence)
    main.add_command(centrality)


# ── analyze ──────────────────────────────────────────────────────────────────


@click.command()
@click.argument("name", required=False)
@click.option("--all", "analyze_all", is_flag=True, help="Analyze all unrated drafts")
@click.option("--limit", "-n", default=50, help="Max drafts to analyze (with --all)")
@click.option("--retry-failed", is_flag=True, help="Re-analyze drafts that previously failed (clears cache)")
@click.option("--dry-run", is_flag=True, help="Show what would be analyzed without making changes")
@click.option("--pre-classify/--no-pre-classify", "pre_classify", default=False,
              help="Pre-filter unrated drafts with local Ollama classifier before Claude")
@pass_cfg_db
def analyze(cfg, db, name: str | None, analyze_all: bool, limit: int, retry_failed: bool, dry_run: bool, pre_classify: bool):
    """Analyze and rate drafts using Claude.

    Use --pre-classify to run the local Ollama classifier first, removing
    irrelevant drafts before spending Claude tokens. Saves ~40% of API costs.
    """
    from ..analyzer import Analyzer

    if dry_run:
        if retry_failed:
            unrated = db.unrated_drafts(limit=limit)
            retryable = []
            for draft in unrated:
                row = db.conn.execute(
                    "SELECT COUNT(*) FROM llm_cache WHERE draft_name = ?",
                    (draft.name,),
                ).fetchone()
                if row[0] > 0:
                    retryable.append(draft)
            console.print(f"[bold yellow]DRY RUN[/]: Would retry [bold]{len(retryable)}[/] previously failed drafts")
            for d in retryable[:20]:
                console.print(f"  - {d.name}")
            if len(retryable) > 20:
                console.print(f"  ... and {len(retryable) - 20} more")
        elif analyze_all:
            unrated = db.unrated_drafts(limit=limit)
            console.print(f"[bold yellow]DRY RUN[/]: Would analyze [bold]{len(unrated)}[/] unrated drafts")
            for d in unrated[:20]:
                console.print(f"  - {d.name}: {d.title[:60]}")
            if len(unrated) > 20:
                console.print(f"  ... and {len(unrated) - 20} more")
        elif name:
            existing = db.get_rating(name)
            status = "re-analyze (already rated)" if existing else "analyze (not yet rated)"
            console.print(f"[bold yellow]DRY RUN[/]: Would {status}: {name}")
        else:
            console.print("Provide a draft name or use --all")
        return

    analyzer = Analyzer(cfg, db)

    if retry_failed:
        # Find drafts that have cache entries but no ratings (failed analyses)
        unrated = db.unrated_drafts(limit=limit)
        retryable = []
        for draft in unrated:
            # Check if there's a cache entry for this draft (it was attempted)
            row = db.conn.execute(
                "SELECT COUNT(*) FROM llm_cache WHERE draft_name = ?",
                (draft.name,),
            ).fetchone()
            if row[0] > 0:
                retryable.append(draft)
        if not retryable:
            console.print("No previously failed drafts to retry.")
        else:
            console.print(f"Retrying [bold]{len(retryable)}[/] previously failed drafts...")
            count = 0
            for draft in retryable:
                rating = analyzer.rate_draft(draft.name, use_cache=False)
                if rating:
                    count += 1
            console.print(f"Successfully re-analyzed [bold green]{count}[/] of {len(retryable)} drafts")
    elif analyze_all:
        if pre_classify:
            # Pre-filter with local Ollama classifier
            try:
                from ..classifier import Classifier
                unrated = db.unrated_drafts(limit=limit)
                if unrated:
                    console.print(f"\n[bold]Pre-classifying {len(unrated)} unrated drafts with Ollama...[/]")
                    clf = Classifier(cfg)
                    draft_dicts = [{"name": d.name, "title": d.title, "abstract": d.abstract} for d in unrated]
                    relevant, irrelevant = clf.classify_batch(draft_dicts, verbose=True)
                    clf.close()

                    if irrelevant:
                        console.print(f"\n  Removing [red]{len(irrelevant)}[/] irrelevant drafts from DB...")
                        for d in irrelevant:
                            db.conn.execute("DELETE FROM drafts WHERE name = ?", (d["name"],))
                        db.conn.commit()
                        console.print(f"  Removed. {len(relevant)} drafts remain for Claude analysis.\n")
            except Exception as e:
                console.print(f"[yellow]Classifier unavailable ({e}), analyzing all[/yellow]")

        count = analyzer.rate_all_unrated(limit=limit)
        console.print(f"Analyzed [bold green]{count}[/] drafts")
    elif name:
        rating = analyzer.rate_draft(name)
        if rating:
            console.print(f"\n[bold green]Rating for {name}:[/]")
            console.print(f"  Score: {rating.composite_score:.1f}")
            console.print(f"  Summary: {rating.summary}")
            console.print(f"  Novelty={rating.novelty} Maturity={rating.maturity} "
                          f"Overlap={rating.overlap} Momentum={rating.momentum} "
                          f"Relevance={rating.relevance}")
        else:
            console.print("[red]Analysis failed[/]")
    else:
        console.print("Provide a draft name or use --all")


# ── ask ──────────────────────────────────────────────────────────────────────


@click.command()
@click.argument("question")
@click.option("--top", "-n", default=5, help="Number of source drafts to use")
@click.option("--cheap/--quality", default=True, help="Use Haiku (cheap) vs Sonnet (quality)")
@pass_cfg_db
def ask(cfg, db, question: str, top: int, cheap: bool):
    """Ask a natural language question about the drafts.

    Examples:
      ietf ask "Which drafts address agent authentication?"
      ietf ask "What are the competing approaches to agent delegation?" --top 10
      ietf ask "How do safety mechanisms work?" --cheap
    """
    from ..search import HybridSearch

    searcher = HybridSearch(cfg, db)
    console.print(f"\n[dim]Searching for relevant drafts...[/]")
    result = searcher.ask(question, top_k=top, cheap=cheap)

    # Display the answer
    console.print()
    console.print("[bold cyan]Answer[/]")
    console.print("[dim]" + "-" * 60 + "[/]")
    console.print(result["answer"])
    console.print()

    # Display source drafts table
    if result["sources"]:
        table = Table(title="Source Drafts")
        table.add_column("#", style="dim", width=3)
        table.add_column("Draft", style="cyan", max_width=50)
        table.add_column("Title", max_width=45)
        table.add_column("Match", width=10)
        table.add_column("Score", justify="right", width=8)

        for i, src in enumerate(result["sources"], 1):
            score_str = f"{src['similarity']:.3f}" if src.get("similarity") else "-"
            table.add_row(
                str(i),
                src["name"],
                src["title"][:45],
                src.get("match_type", ""),
                score_str,
            )

        console.print(table)


# ── compare ──────────────────────────────────────────────────────────────────


@click.command()
@click.argument("names", nargs=-1, required=True)
@pass_cfg_db
def compare(cfg, db, names: tuple[str, ...]):
    """Compare multiple drafts for overlap and unique contributions."""
    from ..analyzer import Analyzer

    analyzer = Analyzer(cfg, db)

    result = analyzer.compare_drafts(list(names))
    if "error" in result:
        console.print(f"[red]{result['error']}[/]")
    else:
        console.print(f"\n[bold cyan]Comparison of {len(result['drafts'])} drafts[/]")
        console.print("[dim]" + "-" * 60 + "[/]")
        console.print(result["text"])


# ── embed ────────────────────────────────────────────────────────────────────


@click.command()
@click.option("--dry-run", is_flag=True, help="Show what would be embedded without making changes")
@pass_cfg_db
def embed(cfg, db, dry_run: bool):
    """Generate embeddings for all drafts (requires Ollama)."""
    if dry_run:
        missing = db.drafts_without_embeddings(limit=10000)
        console.print(f"[bold yellow]DRY RUN[/]: Would embed [bold]{len(missing)}[/] drafts")
        for name in missing[:20]:
            console.print(f"  - {name}")
        if len(missing) > 20:
            console.print(f"  ... and {len(missing) - 20} more")
        return

    from ..embeddings import Embedder

    embedder = Embedder(cfg, db)
    count = embedder.embed_all_missing()
    console.print(f"Embedded [bold green]{count}[/] drafts")


# ── embed-ideas ──────────────────────────────────────────────────────────────


@click.command("embed-ideas")
@click.option("--limit", default=0, help="Max ideas to embed (0=all)")
@click.option("--batch-size", default=50, help="Batch size for Ollama")
@click.option("--dry-run", is_flag=True, help="Show what would be embedded without making changes")
@pass_cfg_db
def embed_ideas(cfg, db, limit: int, batch_size: int, dry_run: bool):
    """Generate embeddings for extracted ideas via Ollama."""
    missing = db.ideas_without_embeddings(limit=limit if limit > 0 else 10000)
    if not missing:
        console.print("All ideas already have embeddings.")
        return

    if dry_run:
        console.print(f"[bold yellow]DRY RUN[/]: Would embed [bold]{len(missing)}[/] ideas in batches of {batch_size}")
        for idea in missing[:20]:
            console.print(f"  - [{idea.get('id', '?')}] {idea['title'][:60]}")
        if len(missing) > 20:
            console.print(f"  ... and {len(missing) - 20} more")
        return

    import numpy as np
    import ollama as ollama_lib
    from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, MofNCompleteColumn

    client = ollama_lib.Client(host=cfg.ollama_url)
    total = len(missing)
    console.print(f"Embedding [bold]{total}[/] ideas in batches of {batch_size}...")

    count = 0
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        MofNCompleteColumn(),
        console=console,
    ) as progress:
        task = progress.add_task("Embedding ideas...", total=total)
        for start in range(0, total, batch_size):
            batch = missing[start:start + batch_size]
            texts = [f"{idea['title']}. {idea['description']}" for idea in batch]
            try:
                resp = client.embed(model=cfg.ollama_embed_model, input=texts)
                for i, idea in enumerate(batch):
                    vec = np.array(resp["embeddings"][i], dtype=np.float32)
                    db.store_idea_embedding(idea["id"], cfg.ollama_embed_model, vec)
                    count += 1
                    progress.advance(task)
            except Exception as e:
                console.print(f"[red]Batch failed: {e}[/]")
                for _ in batch:
                    progress.advance(task)

    console.print(f"Embedded [bold green]{count}[/] ideas")


# ── similar ──────────────────────────────────────────────────────────────────


@click.command()
@click.argument("name")
@click.option("--top", "-n", default=10, help="Number of similar drafts to show")
@pass_cfg_db
def similar(cfg, db, name: str, top: int):
    """Find drafts most similar to a given draft."""
    from ..embeddings import Embedder

    embedder = Embedder(cfg, db)
    results = embedder.find_similar(name, top_n=top)
    if not results:
        console.print(f"[yellow]No similar drafts found (need embeddings — run `ietf embed` first)[/]")
        return

    table = Table(title=f"Drafts similar to {name}")
    table.add_column("Similarity", justify="right", width=10)
    table.add_column("Draft", style="cyan")
    table.add_column("Title")

    for sim_name, score in results:
        draft = db.get_draft(sim_name)
        title = draft.title[:60] if draft else ""
        table.add_row(f"{score:.3f}", sim_name, title)

    console.print(table)


# ── clusters ─────────────────────────────────────────────────────────────────


@click.command()
@click.option("--threshold", "-t", default=0.85, help="Similarity threshold for clustering")
@pass_cfg_db
def clusters(cfg, db, threshold: float):
    """Find clusters of highly similar (potentially overlapping) drafts."""
    from ..embeddings import Embedder

    embedder = Embedder(cfg, db)
    cluster_list = embedder.find_clusters(threshold=threshold)
    if not cluster_list:
        console.print("No clusters found at this threshold.")
        return

    console.print(f"\n[bold]Found {len(cluster_list)} clusters[/] (threshold={threshold})\n")
    for i, cluster in enumerate(cluster_list, 1):
        console.print(f"[bold cyan]Cluster {i}[/] ({len(cluster)} drafts):")
        for name in cluster:
            draft = db.get_draft(name)
            title = draft.title[:60] if draft else ""
            console.print(f"  - {name}  [dim]{title}[/]")
        console.print()


# ── ideas ───────────────────────────────────────────────────────────────


@click.group(invoke_without_command=True)
@click.option("--name", default=None, help="Extract ideas from a specific draft")
@click.option("--all", "extract_all", is_flag=True, help="Extract ideas from all drafts")
@click.option("--limit", "-n", default=50, help="Max drafts to extract (with --all)")
@click.option("--batch", "-b", default=5, help="Drafts per API call (default 5, set 1 for individual)")
@click.option("--cheap/--quality", default=True, help="Use Haiku (cheap) vs Sonnet (quality)")
@click.option("--reextract", is_flag=True, help="Clear existing ideas and re-extract with current prompt")
@click.option("--draft", "reextract_draft", default=None, help="Specific draft to re-extract (with --reextract)")
@click.option("--dry-run", is_flag=True, help="Show what would be extracted without making changes")
@click.pass_context
def ideas(ctx, name: str | None, extract_all: bool, limit: int, batch: int, cheap: bool,
          reextract: bool, reextract_draft: str | None, dry_run: bool):
    """Extract, score, and filter technical ideas from drafts."""
    if ctx.invoked_subcommand is not None:
        return

    cfg = ctx.obj["cfg"]
    db = ctx.obj["db"]

    if dry_run:
        if reextract:
            existing = db.idea_count()
            if reextract_draft:
                ideas_for = db.get_ideas_for_draft(reextract_draft)
                console.print(f"[bold yellow]DRY RUN[/]: Would clear [bold]{len(ideas_for)}[/] ideas for {reextract_draft} and re-extract")
            else:
                console.print(f"[bold yellow]DRY RUN[/]: Would clear all [bold]{existing}[/] ideas and re-extract from up to {limit} drafts")
        elif extract_all:
            missing = db.drafts_without_ideas(limit=limit)
            console.print(f"[bold yellow]DRY RUN[/]: Would extract ideas from [bold]{len(missing)}[/] drafts (batch={batch}, {'cheap' if cheap else 'quality'})")
            for d in missing[:20]:
                console.print(f"  - {d}")
            if len(missing) > 20:
                console.print(f"  ... and {len(missing) - 20} more")
        elif name:
            existing = db.get_ideas_for_draft(name)
            console.print(f"[bold yellow]DRY RUN[/]: Would extract ideas from {name} (currently has {len(existing)} ideas)")
        else:
            console.print("Use --name DRAFT, --all, or a subcommand: ideas score / ideas filter")
        return

    from ..analyzer import Analyzer
    analyzer = Analyzer(cfg, db)

    if reextract:
        # Clear existing ideas, then re-extract
        deleted = db.delete_ideas(draft_name=reextract_draft)
        if reextract_draft:
            console.print(f"Cleared [bold]{deleted}[/] ideas for {reextract_draft}")
            idea_list = analyzer.extract_ideas(reextract_draft, use_cache=True)
            if idea_list:
                console.print(f"Re-extracted [bold green]{len(idea_list)}[/] ideas:")
                for idea in idea_list:
                    console.print(f"  [{idea.get('type', '?')}] [bold]{idea['title']}[/]")
                    console.print(f"    {idea['description']}\n")
            else:
                console.print("[red]Re-extraction failed or no ideas found[/]")
        else:
            console.print(f"Cleared [bold]{deleted}[/] ideas from all drafts")
            count = analyzer.extract_all_ideas(limit=limit, batch_size=batch, cheap=cheap)
            console.print(f"Re-extracted ideas from [bold green]{count}[/] drafts")
    elif extract_all:
        count = analyzer.extract_all_ideas(limit=limit, batch_size=batch, cheap=cheap)
        console.print(f"Extracted ideas from [bold green]{count}[/] drafts")
    elif name:
        idea_list = analyzer.extract_ideas(name)
        if idea_list:
            console.print(f"\n[bold]Ideas from {name}:[/]\n")
            for idea in idea_list:
                console.print(f"  [{idea.get('type', '?')}] [bold]{idea['title']}[/]")
                console.print(f"    {idea['description']}\n")
        else:
            console.print("[red]Extraction failed or no ideas found[/]")
    else:
        console.print("Use --name DRAFT, --all, or a subcommand: ideas score / ideas filter")


@ideas.command("score")
@click.option("--cheap/--quality", default=True, help="Use Haiku (cheap) vs Sonnet (quality)")
@click.option("--batch", "-b", default=20, help="Ideas per API call (default 20)")
@pass_cfg_db
def ideas_score(cfg, db, cheap: bool, batch: int):
    """Score ideas for novelty (1=generic, 5=genuinely novel)."""
    from ..analyzer import Analyzer

    analyzer = Analyzer(cfg, db)
    stats = analyzer.score_idea_novelty(batch_size=batch, cheap=cheap)

    if stats["scored_count"] == 0:
        return

    # Show distribution table
    dist = db.idea_score_distribution()
    table = Table(title="Novelty Score Distribution")
    table.add_column("Score", style="bold", justify="center")
    table.add_column("Label", style="dim")
    table.add_column("Count", justify="right")
    table.add_column("Bar", min_width=30)

    labels = {
        1: "Generic building block",
        2: "Obvious extension",
        3: "Useful but expected",
        4: "Interesting contribution",
        5: "Genuinely novel",
    }
    max_count = max(dist.values()) if dist else 1
    for score in range(1, 6):
        count = dist.get(score, 0)
        bar_len = int(30 * count / max_count) if max_count > 0 else 0
        table.add_row(
            str(score), labels[score], str(count),
            "[green]" + "#" * bar_len + "[/]"
        )

    total = sum(dist.values())
    unscored = db.idea_count() - total
    console.print(table)
    console.print(f"\nTotal scored: [bold]{total}[/] | Unscored: {unscored} | Avg: [bold]{stats['avg_score']:.1f}[/]")


@ideas.command("filter")
@click.option("--min-score", "-m", default=2, help="Remove ideas below this score (default 2)")
@click.option("--dry-run/--execute", default=True, help="Preview (default) or actually delete")
@pass_cfg_db
def ideas_filter(cfg, db, min_score: int, dry_run: bool):
    """Filter out low-novelty ideas by score threshold."""
    candidates = db.ideas_below_score(min_score)
    if not candidates:
        console.print(f"No ideas with novelty_score < {min_score}.")
        return

    # Show what would be removed
    table = Table(
        title=f"Ideas with novelty_score < {min_score} "
              f"({'DRY RUN' if dry_run else 'WILL DELETE'})"
    )
    table.add_column("Score", style="bold", justify="center")
    table.add_column("Idea", style="cyan", max_width=40)
    table.add_column("Draft", max_width=50)
    table.add_column("Description", max_width=60)

    for idea in candidates[:50]:  # Show first 50
        table.add_row(
            str(idea["novelty_score"]),
            idea["title"],
            idea["draft_title"],
            idea["description"][:60] + ("..." if len(idea["description"]) > 60 else ""),
        )

    console.print(table)

    if len(candidates) > 50:
        console.print(f"  ... and {len(candidates) - 50} more")

    console.print(f"\nTotal to remove: [bold red]{len(candidates)}[/] / {db.idea_count()} ideas")

    if not dry_run:
        deleted = db.delete_low_score_ideas(min_score)
        console.print(f"[bold red]Deleted {deleted} low-novelty ideas.[/]")
        console.print(f"Remaining ideas: [bold green]{db.idea_count()}[/]")
    else:
        console.print("[dim]Use --execute to actually delete.[/]")


@ideas.command("convergence")
@click.option("--threshold", "-t", default=0.75, help="SequenceMatcher ratio threshold (0-1)")
@click.option("--limit", "-n", default=50, help="Max results to show")
@click.option("--list-all", is_flag=True, help="List all convergent idea pairs")
@pass_cfg_db
def ideas_convergence(cfg, db, threshold: float, limit: int, list_all: bool):
    """Find cross-org convergent ideas using SequenceMatcher fuzzy matching.

    Groups ideas by fuzzy title similarity, then filters to ideas where
    2+ distinct organizations independently propose similar concepts.
    """
    from collections import defaultdict
    from difflib import SequenceMatcher
    from ..orgs import normalize_org

    all_ideas = db.all_ideas()
    if not all_ideas:
        console.print("[yellow]No ideas extracted yet. Run `ietf ideas --all` first.[/]")
        return

    # Build draft -> org mapping
    draft_orgs: dict[str, set[str]] = defaultdict(set)
    rows = db.conn.execute(
        """SELECT da.draft_name, a.affiliation
        FROM draft_authors da
        JOIN authors a ON da.person_id = a.person_id
        WHERE a.affiliation != ''"""
    ).fetchall()
    for r in rows:
        org = normalize_org(r["affiliation"])
        if org and org != "Independent":
            draft_orgs[r["draft_name"]].add(org)

    # Group similar ideas by fuzzy title matching
    idea_groups: list[dict] = []
    for idea in all_ideas:
        title_lower = idea["title"].lower().strip()
        matched = False
        for group in idea_groups:
            ratio = SequenceMatcher(None, title_lower, group["canonical"]).ratio()
            if ratio >= threshold:
                group["ideas"].append(idea)
                group["drafts"].add(idea["draft_name"])
                group["orgs"].update(draft_orgs.get(idea["draft_name"], set()))
                matched = True
                break
        if not matched:
            idea_groups.append({
                "canonical": title_lower,
                "title": idea["title"],
                "ideas": [idea],
                "drafts": {idea["draft_name"]},
                "orgs": set(draft_orgs.get(idea["draft_name"], set())),
            })

    # Filter to cross-org ideas (2+ orgs)
    cross_org = [g for g in idea_groups if len(g["orgs"]) >= 2]
    cross_org.sort(key=lambda g: (-len(g["orgs"]), -len(g["drafts"])))

    console.print(f"\n[bold]Cross-Organization Idea Convergence[/]")
    console.print(f"Threshold: {threshold} | {len(all_ideas)} ideas | "
                  f"{len(idea_groups)} unique clusters | "
                  f"[bold green]{len(cross_org)}[/] cross-org convergent\n")

    if not cross_org:
        console.print("[yellow]No cross-org convergent ideas at this threshold.[/]")
        return

    show_n = len(cross_org) if list_all else min(limit, len(cross_org))
    table = Table(title=f"Cross-Org Convergent Ideas (showing {show_n} of {len(cross_org)})")
    table.add_column("#", justify="right", width=4)
    table.add_column("Idea", style="bold", max_width=40)
    table.add_column("Orgs", justify="right", width=5)
    table.add_column("Drafts", justify="right", width=6)
    table.add_column("Organizations", max_width=50)

    for rank, g in enumerate(cross_org[:show_n], 1):
        org_list = ", ".join(sorted(g["orgs"])[:5])
        if len(g["orgs"]) > 5:
            org_list += f" +{len(g['orgs']) - 5}"
        table.add_row(
            str(rank), g["title"][:40], str(len(g["orgs"])),
            str(len(g["drafts"])), org_list,
        )

    console.print(table)
    console.print(f"\n[bold]Summary[/]: {len(cross_org)} cross-org convergent ideas "
                  f"out of {len(idea_groups)} unique clusters "
                  f"({100 * len(cross_org) / len(idea_groups):.0f}%)")


# ── dedup-ideas ─────────────────────────────────────────────────────────


@click.command("dedup-ideas")
@click.option("--threshold", "-t", default=0.85, type=float,
              help="Cosine similarity threshold for merging (default 0.85)")
@click.option("--dry-run/--execute", default=True,
              help="Preview merges (default) vs actually delete duplicates")
@click.option("--draft", "draft_name", default=None,
              help="Limit to a single draft name")
@pass_cfg_db
def dedup_ideas(cfg, db, threshold: float, dry_run: bool, draft_name: str | None):
    """Deduplicate similar ideas within each draft using embedding similarity."""
    from ..analyzer import Analyzer

    analyzer = Analyzer(cfg, db)
    mode = "[bold yellow]DRY RUN[/]" if dry_run else "[bold red]EXECUTE[/]"
    console.print(f"\n{mode} — Deduplicating ideas (threshold={threshold})")
    if draft_name:
        console.print(f"Limiting to draft: [bold]{draft_name}[/]")
    console.print()

    result = analyzer.dedup_ideas(
        threshold=threshold, dry_run=dry_run, draft_name=draft_name
    )

    if result["examples"]:
        table = Table(title="Merge Candidates" if dry_run else "Merged Ideas")
        table.add_column("Draft", style="dim", max_width=40)
        table.add_column("Keep", style="green")
        table.add_column("Drop", style="red")
        table.add_column("Similarity", justify="right")

        for ex in result["examples"]:
            table.add_row(
                ex["draft"].split("/")[-1][:40],
                ex["keep"],
                ex["drop"],
                f"{ex['similarity']:.3f}",
            )
        console.print(table)
        console.print()

    action = "Would remove" if dry_run else "Removed"
    console.print(
        f"Ideas before: [bold]{result['total_before']}[/]  |  "
        f"{action}: [bold]{result['merged_count']}[/]  |  "
        f"After: [bold]{result['total_after']}[/]"
    )

    if dry_run and result["merged_count"] > 0:
        console.print(
            "\n[dim]Run with --execute to apply these merges.[/]"
        )


# ── gaps ────────────────────────────────────────────────────────────────


@click.command()
@click.option("--refresh", is_flag=True, help="Re-run gap analysis even if cached")
@click.option("--dry-run", is_flag=True, help="Show existing gaps without running analysis")
@pass_cfg_db
def gaps(cfg, db, refresh: bool, dry_run: bool):
    """Identify gaps in the current draft landscape using Claude."""
    from ..reports import Reporter

    if dry_run:
        existing = db.all_gaps()
        console.print(f"[bold yellow]DRY RUN[/]: {len(existing)} gaps currently identified")
        if refresh:
            console.print("  Would re-run gap analysis via Claude API")
        for i, gap in enumerate(existing if existing and isinstance(existing[0], dict) else [], 1):
            sev = gap.get("severity", "medium").upper()
            console.print(f"  [bold]{i}. {gap['topic']}[/] [{sev}]")
        return

    from ..analyzer import Analyzer
    analyzer = Analyzer(cfg, db)
    reporter = Reporter(cfg, db)

    existing = db.all_gaps()
    if existing and not refresh:
        console.print(f"[bold]{len(existing)} gaps[/] already identified (use --refresh to re-run)\n")
    else:
        gap_list = analyzer.gap_analysis()
        console.print(f"\nIdentified [bold green]{len(gap_list)}[/] gaps\n")
        existing = gap_list

    for i, gap in enumerate(existing if isinstance(existing[0], dict) else [], 1):
        sev = gap.get("severity", "medium").upper()
        console.print(f"  [bold]{i}. {gap['topic']}[/] [{sev}]")
        console.print(f"     {gap['description'][:100]}\n")

    path = reporter.gaps_report()
    console.print(f"Report saved: [bold]{path}[/]")


# ── refs ────────────────────────────────────────────────────────────────


@click.command()
@click.argument("name", required=False)
@click.option("--extract/--no-extract", default=False, help="Extract refs from all drafts with text")
@click.option("--top", "-n", default=30, help="Number of top-referenced items to show")
@click.option("--type", "ref_type", default="rfc", type=click.Choice(["rfc", "draft", "bcp"]),
              help="Reference type to show top results for")
@pass_cfg_db
def refs(cfg, db, name: str | None, extract: bool, top: int, ref_type: str):
    """Parse and show cross-references (RFCs, drafts, BCPs) in draft texts."""
    import re

    if extract:
        missing = db.drafts_without_refs()
        if not missing:
            console.print("[green]All drafts with text already have refs extracted.[/]")
        else:
            console.print(f"Extracting refs from [bold]{len(missing)}[/] drafts...")
            extracted = 0
            for draft_name in missing:
                draft = db.get_draft(draft_name)
                if not draft or not draft.full_text:
                    continue
                found_refs = _extract_refs(draft.full_text, draft.name)
                if found_refs:
                    db.insert_refs(draft_name, found_refs)
                    extracted += 1
            console.print(f"Extracted refs from [bold green]{extracted}[/] drafts")

    if name:
        # Show refs for a specific draft
        draft_refs = db.get_refs_for_draft(name)
        if not draft_refs:
            console.print(f"[yellow]No refs found for {name}. Run `ietf refs --extract` first.[/]")
            return
        table = Table(title=f"References in {name}")
        table.add_column("Type", style="dim", width=6)
        table.add_column("Reference", style="cyan")
        for rt, rid in sorted(draft_refs):
            table.add_row(rt.upper(), rid)
        console.print(table)
    else:
        # Show top-referenced items
        stats = db.ref_stats()
        if stats["total_refs"] == 0:
            console.print("[yellow]No refs extracted yet. Run `ietf refs --extract` first.[/]")
            return

        console.print(f"\n[bold]Reference Stats[/]: {stats['drafts_with_refs']} drafts, "
                      f"{stats['total_refs']} total refs "
                      f"({stats['rfc_refs']} RFC, {stats['draft_refs']} draft, {stats['bcp_refs']} BCP)\n")

        top_items = db.top_referenced(ref_type=ref_type, limit=top)
        table = Table(title=f"Top {len(top_items)} Most-Referenced {ref_type.upper()}s")
        table.add_column("#", justify="right", width=4)
        table.add_column("Reference", style="cyan", width=30)
        table.add_column("Count", justify="right", width=6)
        table.add_column("Referenced By", max_width=60)
        for rank, (rid, cnt, drafts) in enumerate(top_items, 1):
            label = f"RFC {rid}" if ref_type == "rfc" else rid
            draft_list = ", ".join(d.replace("draft-", "")[:25] for d in drafts[:4])
            if len(drafts) > 4:
                draft_list += f" +{len(drafts) - 4}"
            table.add_row(str(rank), label, str(cnt), draft_list)
        console.print(table)


def _extract_refs(text: str, self_name: str) -> list[tuple[str, str]]:
    """Extract RFC, draft, and BCP references from draft full text."""
    import re

    refs_set: set[tuple[str, str]] = set()

    # RFC references: RFC 1234, RFC1234, [RFC1234], [RFC 1234]
    for m in re.finditer(r'\[?RFC\s*(\d{4,})\]?', text, re.IGNORECASE):
        refs_set.add(("rfc", m.group(1)))

    # BCP references: BCP 14, BCP14, [BCP14]
    for m in re.finditer(r'\[?BCP\s*(\d+)\]?', text, re.IGNORECASE):
        refs_set.add(("bcp", m.group(1)))

    # Draft references: draft-ietf-something-name
    for m in re.finditer(r'(draft-[\w][\w-]+[\w])', text, re.IGNORECASE):
        draft_ref = m.group(1).lower()
        # Strip trailing version numbers (e.g., draft-foo-bar-03 -> draft-foo-bar)
        draft_ref = re.sub(r'-\d{2,}$', '', draft_ref)
        # Don't reference self
        if draft_ref != self_name:
            refs_set.add(("draft", draft_ref))

    return list(refs_set)


# ── trends ─────────────────────────────────────────────────────────────


@click.command()
@click.option("--category", "-c", help="Filter to a specific category")
@click.option("--json-out", is_flag=True, help="Also output JSON for visualization")
@pass_cfg_db
def trends(cfg, db, category: str | None, json_out: bool):
    """Show category trend analysis — monthly breakdown with growth rates."""
    import json as json_mod
    from collections import defaultdict

    pairs = db.drafts_with_ratings(limit=500)
    all_drafts = db.list_drafts(limit=500, order_by="time ASC")

    if not pairs:
        console.print("[yellow]No rated drafts. Run `ietf analyze --all` first.[/]")
        return

    # Build rating lookup
    rating_map = {draft.name: rating for draft, rating in pairs}

    # Collect monthly counts per category
    monthly: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
    all_cats: set[str] = set()
    for d in all_drafts:
        month = d.time[:7] if d.time else "unknown"
        r = rating_map.get(d.name)
        if r:
            for c in r.categories:
                if category and c.lower() != category.lower():
                    continue
                monthly[month][c] += 1
                all_cats.add(c)

    if not all_cats:
        console.print(f"[yellow]No data for category '{category}'[/]" if category
                      else "[yellow]No category data found.[/]")
        return

    months = sorted(m for m in monthly.keys() if m != "unknown")
    cats = sorted(all_cats)

    # Compute cumulative and growth
    rows_data = []
    cumulative: dict[str, int] = defaultdict(int)
    prev_count: dict[str, int] = defaultdict(int)

    for month in months:
        for cat in cats:
            count = monthly[month].get(cat, 0)
            cumulative[cat] += count
            growth = 0.0
            if prev_count[cat] > 0:
                growth = ((count - prev_count[cat]) / prev_count[cat]) * 100
            rows_data.append({
                "month": month,
                "category": cat,
                "count": count,
                "cumulative": cumulative[cat],
                "growth_rate": growth,
            })
            prev_count[cat] = count

    # Display summary table
    console.print(f"\n[bold]Category Trends[/] — {len(months)} months, {len(cats)} categories\n")

    # Show per-category totals and recent momentum
    table = Table(title="Category Growth Summary")
    table.add_column("Category", style="cyan")
    table.add_column("Total", justify="right", width=6)
    table.add_column("Last 3mo", justify="right", width=8)
    table.add_column("Prev 3mo", justify="right", width=8)
    table.add_column("Growth", justify="right", width=8)

    recent_months = months[-3:] if len(months) >= 3 else months
    prev_months = months[-6:-3] if len(months) >= 6 else []

    for cat in cats:
        total = cumulative[cat]
        recent = sum(monthly[m].get(cat, 0) for m in recent_months)
        prev = sum(monthly[m].get(cat, 0) for m in prev_months) if prev_months else 0
        if prev > 0:
            growth_str = f"{((recent - prev) / prev) * 100:+.0f}%"
        elif recent > 0:
            growth_str = "new"
        else:
            growth_str = "-"
        table.add_row(cat, str(total), str(recent), str(prev) if prev_months else "-", growth_str)

    console.print(table)

    # Monthly detail
    console.print(f"\n[bold]Monthly Breakdown[/]\n")
    detail_table = Table()
    detail_table.add_column("Month", style="dim", width=8)
    for cat in cats:
        detail_table.add_column(cat[:14], justify="right", width=max(6, len(cat[:14])))
    detail_table.add_column("Total", justify="right", width=6, style="bold")

    for month in months:
        row = [month]
        total = 0
        for cat in cats:
            c = monthly[month].get(cat, 0)
            total += c
            row.append(str(c) if c else "")
        row.append(str(total))
        detail_table.add_row(*row)

    console.print(detail_table)

    # Optional JSON output
    if json_out:
        out_path = Path(cfg.data_dir) / "reports" / "trends.json"
        out_path.write_text(json_mod.dumps(rows_data, indent=2))
        console.print(f"\nJSON saved: [bold]{out_path}[/]")


# ── status ──────────────────────────────────────────────────────────────


@click.command()
@click.option("--wg", "-w", help="Filter to a specific WG")
def status(wg: str | None):
    """Show WG adoption status — which drafts have institutional backing."""
    import json as json_mod
    from collections import defaultdict

    cfg = _get_config()
    db = Database(cfg)

    try:
        all_status = db.draft_adoption_status()
        total = len(all_status)
        adopted = [s for s in all_status if s["wg_adopted"]]
        individual = [s for s in all_status if not s["wg_adopted"]]
        irtf = [s for s in all_status if s["stream"] == "irtf"]

        console.print(f"\n[bold]Draft Adoption Status[/]: {total} total drafts\n")
        console.print(f"  WG-adopted (draft-ietf-*): [bold green]{len(adopted)}[/] ({len(adopted)/total*100:.1f}%)")
        console.print(f"  IRTF (draft-irtf-*): [bold blue]{len(irtf)}[/]")
        console.print(f"  Individual: [bold]{len(individual)}[/] ({len(individual)/total*100:.1f}%)\n")

        # WG breakdown
        wg_groups: dict[str, list[dict]] = defaultdict(list)
        for s in adopted:
            wg_groups[s["wg_name"]].append(s)

        if wg:
            # Show drafts for a specific WG
            wg_drafts = wg_groups.get(wg, [])
            if not wg_drafts:
                console.print(f"[yellow]No WG-adopted drafts for '{wg}'[/]")
                return
            table = Table(title=f"WG '{wg}' Drafts ({len(wg_drafts)})")
            table.add_column("Date", style="dim", width=10)
            table.add_column("Name", style="cyan")
            table.add_column("Title", max_width=50)
            for s in sorted(wg_drafts, key=lambda x: x["time"] or ""):
                table.add_row(s["time"][:10] if s["time"] else "", s["name"], s["title"][:50])
            console.print(table)
        else:
            # Show WG summary
            table = Table(title=f"Working Groups with AI/Agent Drafts ({len(wg_groups)} WGs)")
            table.add_column("#", justify="right", width=4)
            table.add_column("WG", style="cyan", width=12)
            table.add_column("Drafts", justify="right", width=6)
            table.add_column("Draft Names", max_width=60)

            for rank, (wg_name, drafts) in enumerate(
                sorted(wg_groups.items(), key=lambda x: -len(x[1])), 1
            ):
                draft_list = ", ".join(d["name"].replace("draft-ietf-", "")[:30] for d in drafts[:4])
                if len(drafts) > 4:
                    draft_list += f" +{len(drafts) - 4}"
                table.add_row(str(rank), wg_name, str(len(drafts)), draft_list)

            console.print(table)

        # Score comparison
        pairs = db.drafts_with_ratings(limit=500)
        if pairs:
            adopted_names = {s["name"] for s in adopted}
            adopted_scores = [r.composite_score for d, r in pairs if d.name in adopted_names]
            individual_scores = [r.composite_score for d, r in pairs if d.name not in adopted_names]

            if adopted_scores and individual_scores:
                console.print(f"\n[bold]Score Comparison[/]:")
                avg_adopted = sum(adopted_scores) / len(adopted_scores)
                avg_individual = sum(individual_scores) / len(individual_scores)
                console.print(f"  WG-adopted avg score: [bold green]{avg_adopted:.2f}[/] ({len(adopted_scores)} rated)")
                console.print(f"  Individual avg score: [bold]{avg_individual:.2f}[/] ({len(individual_scores)} rated)")

        # Check gap coverage
        gaps_list = db.all_gaps()
        if gaps_list:
            gap_cats = {g["category"].lower() for g in gaps_list}
            adopted_cats: set[str] = set()
            for d, r in pairs:
                if d.name in adopted_names:
                    for c in r.categories:
                        adopted_cats.add(c.lower())

            covered = gap_cats & adopted_cats
            uncovered = gap_cats - adopted_cats
            console.print(f"\n[bold]Gap Coverage by WG-Adopted Work[/]:")
            console.print(f"  Gap categories with WG backing: {len(covered)}")
            if covered:
                console.print(f"    {', '.join(sorted(covered))}")
            console.print(f"  Gap categories without WG backing: {len(uncovered)}")
            if uncovered:
                console.print(f"    [yellow]{', '.join(sorted(uncovered))}[/]")

    finally:
        db.close()


# ── revisions ──────────────────────────────────────────────────────────


@click.command()
@click.option("--org", "-o", help="Filter to a specific organization")
@click.option("--top", "-n", default=20, help="Number of orgs to show")
def revisions(org: str | None, top: int):
    """Analyze draft revision velocity — who iterates vs fire-and-forget."""
    from collections import defaultdict
    from ..orgs import normalize_org

    cfg = _get_config()
    db = Database(cfg)

    try:
        all_revs = db.revision_velocity()
        total = len(all_revs)
        at_00 = sum(1 for r in all_revs if r["rev_int"] == 0)
        avg_rev = sum(r["rev_int"] for r in all_revs) / total if total else 0

        console.print(f"\n[bold]Draft Revision Velocity[/]: {total} drafts\n")
        console.print(f"  Average revision: [bold]{avg_rev:.2f}[/]")
        console.print(f"  At -00 (first draft): [bold]{at_00}[/] ({at_00/total*100:.1f}%)")
        console.print(f"  Iterated (rev >= 01): [bold]{total - at_00}[/] ({(total-at_00)/total*100:.1f}%)")
        console.print(f"  Highly iterated (rev >= 05): [bold]{sum(1 for r in all_revs if r['rev_int'] >= 5)}[/]\n")

        # Get per-org stats using normalized org names
        aff_rows = db.conn.execute(
            "SELECT da.draft_name, a.affiliation FROM draft_authors da "
            "JOIN authors a ON da.person_id = a.person_id "
            "WHERE a.affiliation != ''"
        ).fetchall()

        # Map draft -> rev
        draft_rev = {r["name"]: r["rev_int"] for r in all_revs}

        # Group drafts by normalized org (deduped)
        org_drafts: dict[str, set[str]] = defaultdict(set)
        for row in aff_rows:
            norm = normalize_org(row["affiliation"])
            if norm:
                org_drafts[norm].add(row["draft_name"])

        if org:
            # Show drafts for a specific org
            drafts = org_drafts.get(org, set())
            if not drafts:
                console.print(f"[yellow]No drafts for '{org}'[/]")
                return
            table = Table(title=f"'{org}' Drafts by Revision ({len(drafts)})")
            table.add_column("Rev", justify="right", width=4)
            table.add_column("Name", style="cyan", max_width=50)
            table.add_column("Title", max_width=40)
            draft_details = [(d, draft_rev.get(d, 0)) for d in drafts]
            for name, rev in sorted(draft_details, key=lambda x: -x[1]):
                title_row = next((r["title"] for r in all_revs if r["name"] == name), "")
                table.add_row(f"-{rev:02d}", name, title_row[:40])
            console.print(table)
        else:
            # Show org summary
            org_stats = []
            for org_name, drafts in org_drafts.items():
                if len(drafts) < 3:
                    continue
                revs = [draft_rev.get(d, 0) for d in drafts]
                n_00 = sum(1 for r in revs if r == 0)
                org_stats.append({
                    "org": org_name,
                    "drafts": len(drafts),
                    "avg_rev": sum(revs) / len(revs),
                    "at_00": n_00,
                    "pct_00": n_00 / len(drafts) * 100,
                    "max_rev": max(revs),
                })

            org_stats.sort(key=lambda x: -x["drafts"])

            table = Table(title=f"Revision Velocity by Organization (>= 3 drafts, top {top})")
            table.add_column("#", justify="right", width=4)
            table.add_column("Organization", style="cyan", width=28)
            table.add_column("Drafts", justify="right", width=6)
            table.add_column("Avg Rev", justify="right", width=8)
            table.add_column("At -00", justify="right", width=6)
            table.add_column("%-00", justify="right", width=6)
            table.add_column("Max", justify="right", width=4)

            for rank, s in enumerate(org_stats[:top], 1):
                table.add_row(
                    str(rank), s["org"][:28], str(s["drafts"]),
                    f"{s['avg_rev']:.2f}", str(s["at_00"]),
                    f"{s['pct_00']:.0f}%", str(s["max_rev"]),
                )
            console.print(table)

            # Highlight the fire-and-forget vs iterators narrative
            high_00 = [s for s in org_stats if s["pct_00"] >= 70 and s["drafts"] >= 5]
            iterators = [s for s in org_stats if s["avg_rev"] >= 3.0 and s["drafts"] >= 3]

            if high_00:
                console.print("\n[bold]Fire-and-Forget[/] (>= 70% at -00, >= 5 drafts):")
                for s in high_00:
                    console.print(f"  {s['org']}: {s['at_00']}/{s['drafts']} at -00 ({s['pct_00']:.0f}%)")

            if iterators:
                console.print("\n[bold]Active Iterators[/] (avg revision >= 3.0):")
                for s in iterators:
                    console.print(f"  {s['org']}: avg rev {s['avg_rev']:.1f}, max -{s['max_rev']:02d}")

        # Generate report
        from ..reports import Reporter
        reporter = Reporter(cfg, db)
        path = reporter.revisions_report()
        console.print(f"\nReport saved: [bold]{path}[/]")

    finally:
        db.close()


# ── idea-overlap ────────────────────────────────────────────────────────


@click.command("idea-overlap")
@click.option("--threshold", "-t", default=0.75, help="Title similarity threshold (0-1)")
@click.option("--limit", "-n", default=50, help="Max results to show")
def idea_overlap(threshold: float, limit: int):
    """Find ideas that appear across multiple organizations."""
    from collections import defaultdict
    from difflib import SequenceMatcher
    from ..orgs import normalize_org

    cfg = _get_config()
    db = Database(cfg)

    try:
        all_ideas = db.all_ideas()
        if not all_ideas:
            console.print("[yellow]No ideas extracted yet. Run `ietf ideas --all` first.[/]")
            return

        # Build draft -> org mapping
        draft_orgs: dict[str, set[str]] = defaultdict(set)
        rows = db.conn.execute(
            """SELECT da.draft_name, a.affiliation
            FROM draft_authors da
            JOIN authors a ON da.person_id = a.person_id
            WHERE a.affiliation != ''"""
        ).fetchall()
        for r in rows:
            org = normalize_org(r["affiliation"])
            if org and org != "Independent":
                draft_orgs[r["draft_name"]].add(org)

        # Group similar ideas (same logic as ideas_report but tracking orgs)
        idea_groups: list[dict] = []
        for idea in all_ideas:
            title_lower = idea["title"].lower().strip()
            matched = False
            for group in idea_groups:
                ratio = SequenceMatcher(None, title_lower, group["canonical"]).ratio()
                if ratio >= threshold:
                    group["ideas"].append(idea)
                    group["drafts"].add(idea["draft_name"])
                    group["orgs"].update(draft_orgs.get(idea["draft_name"], set()))
                    matched = True
                    break
            if not matched:
                idea_groups.append({
                    "canonical": title_lower,
                    "title": idea["title"],
                    "ideas": [idea],
                    "drafts": {idea["draft_name"]},
                    "orgs": set(draft_orgs.get(idea["draft_name"], set())),
                })

        # Filter to cross-org ideas (2+ orgs)
        cross_org = [g for g in idea_groups if len(g["orgs"]) >= 2]
        cross_org.sort(key=lambda g: (-len(g["orgs"]), -len(g["drafts"])))

        console.print(f"\n[bold]Cross-Organization Idea Overlap[/]")
        console.print(f"{len(all_ideas)} ideas, {len(idea_groups)} unique, "
                      f"[bold green]{len(cross_org)}[/] appear across 2+ orgs\n")

        if not cross_org:
            console.print("[yellow]No cross-org idea overlap found at this threshold.[/]")
            return

        table = Table(title=f"Ideas Shared Across Organizations (top {min(limit, len(cross_org))})")
        table.add_column("#", justify="right", width=4)
        table.add_column("Idea", style="bold", max_width=40)
        table.add_column("Orgs", justify="right", width=5)
        table.add_column("Drafts", justify="right", width=6)
        table.add_column("Organizations", max_width=50)

        for rank, g in enumerate(cross_org[:limit], 1):
            org_list = ", ".join(sorted(g["orgs"])[:5])
            if len(g["orgs"]) > 5:
                org_list += f" +{len(g['orgs']) - 5}"
            table.add_row(
                str(rank), g["title"][:40], str(len(g["orgs"])),
                str(len(g["drafts"])), org_list,
            )

        console.print(table)

        # Also generate the report
        from ..reports import Reporter
        reporter = Reporter(cfg, db)
        path = reporter.idea_overlap_report()
        console.print(f"\nReport saved: [bold]{path}[/]")

    finally:
        db.close()


# ── co-occurrence ──────────────────────────────────────────────────────


@click.command("co-occurrence")
def co_occurrence():
    """Category co-occurrence matrix — which categories appear together."""
    from collections import defaultdict

    cfg = _get_config()
    db = Database(cfg)

    try:
        pairs = db.drafts_with_ratings(limit=500)
        total = len(pairs)
        multi_cat = sum(1 for d, r in pairs if len(r.categories) > 1)

        console.print(f"\n[bold]Category Co-occurrence Analysis[/]: {total} drafts\n")
        console.print(f"  Multi-category drafts: [bold]{multi_cat}[/] ({multi_cat/total*100:.1f}%)\n")

        # Build counts
        cat_counts: dict[str, int] = defaultdict(int)
        cooccur: dict[tuple[str, str], int] = defaultdict(int)
        for d, r in pairs:
            for c in r.categories:
                cat_counts[c] += 1
            for i, c1 in enumerate(r.categories):
                for c2 in r.categories[i + 1:]:
                    key = tuple(sorted([c1, c2]))
                    cooccur[key] += 1

        # Top co-occurrences
        table = Table(title="Top 15 Category Co-occurrences")
        table.add_column("#", justify="right", width=4)
        table.add_column("Category A", style="cyan", width=22)
        table.add_column("Category B", style="cyan", width=22)
        table.add_column("Count", justify="right", width=6)

        top_pairs = sorted(cooccur.items(), key=lambda x: -x[1])[:15]
        for rank, ((c1, c2), n) in enumerate(top_pairs, 1):
            table.add_row(str(rank), c1, c2, str(n))
        console.print(table)

        # AI safety isolation check
        safety_cooccur = {k: v for k, v in cooccur.items() if "AI safety/alignment" in k}
        if safety_cooccur:
            console.print("\n[bold]AI Safety/Alignment Co-occurrences[/]:")
            for (c1, c2), n in sorted(safety_cooccur.items(), key=lambda x: -x[1]):
                other = c2 if c1 == "AI safety/alignment" else c1
                console.print(f"  {n:>3d}  + {other}")

        # Generate report
        from ..reports import Reporter
        reporter = Reporter(cfg, db)
        path = reporter.co_occurrence_report()
        console.print(f"\nReport saved: [bold]{path}[/]")

    finally:
        db.close()


# ── centrality ─────────────────────────────────────────────────────────


@click.command()
@click.option("--top", "-n", default=20, help="Number of results to show")
def centrality(top: int):
    """Author network centrality — bridge-builders and key connectors."""
    import networkx as nx
    from collections import defaultdict
    from ..orgs import normalize_org

    cfg = _get_config()
    db = Database(cfg)

    try:
        # Build co-authorship graph
        rows = db.conn.execute(
            """SELECT da1.person_id as p1, da2.person_id as p2, COUNT(*) as shared
            FROM draft_authors da1
            JOIN draft_authors da2 ON da1.draft_name = da2.draft_name
                AND da1.person_id < da2.person_id
            GROUP BY da1.person_id, da2.person_id"""
        ).fetchall()

        G = nx.Graph()
        for r in rows:
            G.add_edge(r[0], r[1], weight=r[2])

        persons = db.conn.execute(
            "SELECT person_id, name, affiliation FROM authors"
        ).fetchall()
        person_info = {r[0]: (r[1], normalize_org(r[2])) for r in persons}

        console.print(f"\n[bold]Author Network Analysis[/]: {G.number_of_nodes()} authors, {G.number_of_edges()} co-authorship edges\n")

        # Cross-org vs intra-org edges
        chinese_orgs = {
            "Huawei", "China Mobile", "China Telecom", "China Unicom",
            "ZTE Corporation", "Tsinghua University", "BUPT",
            "Pengcheng Laboratory", "CAICT", "AsiaInfo",
            "Zhongguancun Laboratory", "CNIC, CAS",
            "Tsinghua Shenzhen International Graduate School & Pengcheng Laboratory",
            "Huazhong University of Science and Technology",
        }
        cross_org = intra_org = cross_divide = 0
        for u, v in G.edges():
            _, org_u = person_info.get(u, ("?", ""))
            _, org_v = person_info.get(v, ("?", ""))
            if org_u and org_v:
                if org_u == org_v:
                    intra_org += 1
                else:
                    cross_org += 1
                    if (org_u in chinese_orgs) != (org_v in chinese_orgs):
                        cross_divide += 1

        total_edges = cross_org + intra_org
        console.print(f"  Intra-org edges: [bold]{intra_org}[/] ({intra_org/total_edges*100:.1f}%)")
        console.print(f"  Cross-org edges: [bold]{cross_org}[/] ({cross_org/total_edges*100:.1f}%)")
        console.print(f"  Cross Chinese-Western edges: [bold]{cross_divide}[/]")

        avg_clustering = nx.average_clustering(G)
        components = list(nx.connected_components(G))
        console.print(f"  Clustering coefficient: [bold]{avg_clustering:.3f}[/]")
        console.print(f"  Connected components: [bold]{len(components)}[/], largest: {len(max(components, key=len))}\n")

        # Betweenness centrality
        bc = nx.betweenness_centrality(G)

        table = Table(title=f"Top {top} Authors by Betweenness Centrality")
        table.add_column("#", justify="right", width=4)
        table.add_column("Author", style="bold", width=28)
        table.add_column("Organization", style="cyan", width=20)
        table.add_column("BC Score", justify="right", width=8)
        table.add_column("Degree", justify="right", width=6)
        table.add_column("CN/West", justify="center", width=8)

        top_bc = sorted(bc.items(), key=lambda x: -x[1])[:top]
        for rank, (pid, score) in enumerate(top_bc, 1):
            name, org = person_info.get(pid, ("?", "?"))
            degree = G.degree(pid)
            cn = sum(1 for n in G.neighbors(pid) if person_info.get(n, ("", ""))[1] in chinese_orgs)
            west = sum(1 for n in G.neighbors(pid) if person_info.get(n, ("", ""))[1] not in chinese_orgs and person_info.get(n, ("", ""))[1])
            table.add_row(str(rank), name[:28], org[:20], f"{score:.4f}", str(degree), f"{cn}/{west}")

        console.print(table)

        # Bridge-builders
        bridges = []
        for pid in G.nodes():
            name, org = person_info.get(pid, ("?", ""))
            cn = sum(1 for n in G.neighbors(pid) if person_info.get(n, ("", ""))[1] in chinese_orgs)
            west = sum(1 for n in G.neighbors(pid) if person_info.get(n, ("", ""))[1] not in chinese_orgs and person_info.get(n, ("", ""))[1])
            if cn > 0 and west > 0:
                bridges.append((pid, name, org, bc.get(pid, 0), cn, west))

        bridges.sort(key=lambda x: -x[3])
        console.print(f"\n[bold]Cross-Divide Bridge-Builders[/] ({len(bridges)} people with neighbors in both blocs):\n")
        for pid, name, org, bc_score, cn, west in bridges[:10]:
            console.print(f"  [bold]{name}[/] ({org}): BC={bc_score:.4f}, CN neighbors={cn}, Western={west}")

        # Generate report
        from ..reports import Reporter
        reporter = Reporter(cfg, db)
        path = reporter.centrality_report()
        console.print(f"\nReport saved: [bold]{path}[/]")

    finally:
        db.close()
