"""Author-related data access functions."""
from __future__ import annotations

import re
from collections import Counter, defaultdict
from typing import TypedDict

from ietf_analyzer.db import Database
from webui.data._shared import _cached


class AuthorInfo(TypedDict):
    """Author entry from :func:`get_top_authors`."""
    name: str
    affiliation: str
    draft_count: int
    drafts: list[str]
    person_id: int

class AuthorNetworkNode(TypedDict):
    """Node in the author network graph."""
    id: str
    name: str
    org: str
    draft_count: int
    avg_score: float
    drafts: list[str]

class AuthorNetworkEdge(TypedDict):
    """Edge in the author network graph."""
    source: str
    target: str
    weight: int

class AuthorCluster(TypedDict):
    """Cluster in the author network."""
    id: int
    members: list[str]
    org_mix: dict[str, int]
    size: int
    drafts: list[dict[str, str]]
    draft_count: int

class AuthorNetwork(TypedDict):
    """Full author network from :func:`get_author_network_full`."""
    nodes: list[AuthorNetworkNode]
    edges: list[AuthorNetworkEdge]
    clusters: list[AuthorCluster]

def get_top_authors(db: Database, limit: int = 30) -> list[AuthorInfo]:
    """Return top authors by draft count."""
    rows = db.top_authors(limit=limit)
    return [
        {"name": name, "affiliation": aff, "draft_count": cnt, "drafts": drafts,
         "person_id": pid}
        for name, aff, cnt, drafts, pid in rows
    ]

def get_org_data(db: Database, limit: int = 20) -> list[dict]:
    """Return organization contribution data."""
    rows = db.top_orgs(limit=limit)
    return [
        {"org": org, "author_count": authors, "draft_count": drafts}
        for org, authors, drafts in rows
    ]

def get_coauthor_network(db: Database, min_shared: int = 1) -> dict:
    """Return co-authorship network data for force-directed graph.

    Returns {nodes: [{id, name, org, draft_count}], edges: [{source, target, weight}]}
    """
    pairs = db.coauthor_pairs()
    top = db.top_authors(limit=100)

    # Build node set from authors who have co-authorships
    author_info = {name: {"org": aff, "draft_count": cnt} for name, aff, cnt, _, _pid in top}
    node_set = set()
    edges = []
    for a, b, shared in pairs:
        if shared >= min_shared:
            node_set.add(a)
            node_set.add(b)
            edges.append({"source": a, "target": b, "weight": shared})

    nodes = []
    for name in node_set:
        info = author_info.get(name, {"org": "", "draft_count": 1})
        nodes.append({
            "id": name,
            "name": name,
            "org": info["org"],
            "draft_count": info["draft_count"],
        })

    return {"nodes": nodes, "edges": edges}

def get_author_detail(db: Database, person_id: int) -> dict | None:
    """Return author detail with drafts, ratings, and co-authors."""
    author = db.get_author_by_id(person_id)
    if not author:
        return None

    draft_names = db.get_author_drafts(person_id)
    drafts_map = db.get_drafts_by_names(draft_names)

    # Get ratings for each draft
    rated = db.drafts_with_ratings(limit=2000)
    rating_map = {d.name: r for d, r in rated}

    drafts = []
    for dn in draft_names:
        d = drafts_map.get(dn)
        if not d:
            continue
        r = rating_map.get(dn)
        drafts.append({
            "name": d.name,
            "title": d.title,
            "date": d.date,
            "states": d.states,
            "categories": r.categories if r else [],
            "score": round(r.composite_score, 2) if r else None,
            "novelty": r.novelty if r else None,
            "maturity": r.maturity if r else None,
            "relevance": r.relevance if r else None,
        })

    coauthors = db.get_coauthors(person_id)

    return {
        "person_id": author["person_id"],
        "name": author["name"],
        "affiliation": author["affiliation"],
        "ascii_name": author["ascii_name"],
        "drafts": drafts,
        "coauthors": coauthors,
    }


def get_cross_org_data(db: Database, limit: int = 20) -> list[dict]:
    """Return cross-org collaboration pairs."""
    rows = db.cross_org_collaborations(limit=limit)
    return [
        {"org_a": a, "org_b": b, "shared_drafts": cnt}
        for a, b, cnt in rows
    ]

def get_author_network_full(db: Database) -> AuthorNetwork:
    """Return author network (cached for 5 min)."""
    return _cached("author_network", lambda: _compute_author_network_full(db))

def _compute_author_network_full(db: Database) -> AuthorNetwork:
    """Return enriched co-authorship network with avg scores and cluster info.

    Returns {
        nodes: [{id, name, org, draft_count, avg_score, drafts: [name,...]}],
        edges: [{source, target, weight}],
        clusters: [{id, members: [name,...], org_mix: {org: count}, size}],
    }
    """
    pairs = db.coauthor_pairs()
    top = db.top_authors(limit=500)

    # Build rating lookup for avg scores
    rated = db.drafts_with_ratings(limit=2000)
    draft_score = {d.name: r.composite_score for d, r in rated}

    # Author info map
    author_info = {}
    for name, aff, cnt, drafts, _pid in top:
        scores = [draft_score[dn] for dn in drafts if dn in draft_score]
        avg = round(sum(scores) / len(scores), 2) if scores else 0
        author_info[name] = {
            "org": aff, "draft_count": cnt, "drafts": drafts, "avg_score": avg
        }

    # Build node set: authors with meaningful collaboration (2+ shared drafts)
    node_set = set()
    edges = []
    for a, b, shared in pairs:
        if shared >= 2:
            node_set.add(a)
            node_set.add(b)
            edges.append({"source": a, "target": b, "weight": shared})

    # Also include authors with 3+ drafts even if no co-authorships
    for name, info in author_info.items():
        if info["draft_count"] >= 3:
            node_set.add(name)

    nodes = []
    for name in node_set:
        info = author_info.get(name, {"org": "", "draft_count": 1, "drafts": [], "avg_score": 0})
        nodes.append({
            "id": name,
            "name": name,
            "org": info["org"],
            "draft_count": info["draft_count"],
            "avg_score": info["avg_score"],
            "drafts": info["drafts"][:8],  # cap for JSON size
        })

    # Cluster detection via connected components (BFS)
    adjacency: dict[str, set[str]] = defaultdict(set)
    for e in edges:
        adjacency[e["source"]].add(e["target"])
        adjacency[e["target"]].add(e["source"])

    visited: set[str] = set()
    clusters = []

    # Batch-load all drafts referenced by authors (avoid N+1 in cluster loop)
    _all_dn = set()
    for _ai in author_info.values():
        _all_dn.update(_ai.get("drafts", []))
    _all_drafts_map = db.get_drafts_by_names(list(_all_dn))

    for node in sorted(node_set):
        if node in visited:
            continue
        component: list[str] = []
        queue = [node]
        while queue:
            current = queue.pop(0)
            if current in visited:
                continue
            visited.add(current)
            component.append(current)
            for neighbor in adjacency.get(current, []):
                if neighbor not in visited:
                    queue.append(neighbor)

        if len(component) >= 2:
            org_mix: dict[str, int] = Counter()
            member_orgs: dict[str, str] = {}
            cluster_drafts: dict[str, str] = {}  # name -> title
            for m in component:
                org = author_info.get(m, {}).get("org", "")
                if org:
                    org_mix[org] += 1
                    member_orgs[m] = org
                for dn in author_info.get(m, {}).get("drafts", []):
                    if dn not in cluster_drafts:
                        d = _all_drafts_map.get(dn)
                        cluster_drafts[dn] = d.title[:80] if d else dn
            clusters.append({
                "id": len(clusters),
                "members": component,
                "member_orgs": member_orgs,
                "org_mix": dict(org_mix.most_common()),
                "size": len(component),
                "drafts": [{"name": n, "title": t} for n, t in list(cluster_drafts.items())],
                "draft_count": len(cluster_drafts),
            })

    clusters.sort(key=lambda c: c["size"], reverse=True)

    # Generate meaningful names for clusters
    for cl in clusters:
        cl["name"] = _author_cluster_name(cl)

    return {"nodes": nodes, "edges": edges, "clusters": clusters}

def _normalize_org(name: str) -> str:
    """Shorten verbose org names for display."""
    # Remove common suffixes
    for suffix in (", Inc.", " Inc.", ", Ltd.", " Ltd.", " Co.", " Technologies",
                   " Corporation", " Corp.", " Limited", " GmbH", " AG",
                   " Europe Ltd", " Research", " Systems"):
        name = name.replace(suffix, "")
    return name.strip().rstrip(",").rstrip("&").rstrip()

def _author_cluster_name(cluster: dict) -> str:
    """Derive a meaningful name for an author cluster from orgs and draft titles."""
    # Org part: top 1-2 orgs, normalized
    raw_orgs = list(cluster.get("org_mix", {}).keys())
    orgs = []
    seen_short: set[str] = set()
    for o in raw_orgs:
        short = _normalize_org(o)
        if short.lower() not in seen_short:
            seen_short.add(short.lower())
            orgs.append(short)
    if len(orgs) >= 2:
        org_label = f"{orgs[0]} + {orgs[1]}"
    elif orgs:
        org_label = orgs[0]
    else:
        # Fall back to first member's last name
        members = cluster.get("members", [])
        org_label = members[0].split()[-1] if members else "Unknown"

    # Topic part: extract common keywords from draft titles
    stopwords = {
        "a", "an", "the", "of", "for", "in", "to", "and", "on", "with",
        "using", "based", "draft", "internet", "ietf", "protocol", "framework",
        "requirements", "architecture", "considerations", "use", "cases", "via",
        "towards", "over", "from", "into", "between", "specification", "extension",
        "extensions", "mechanisms", "mechanism", "version", "new", "general",
    }
    word_counts: Counter = Counter()
    for d in cluster.get("drafts", []):
        title = d.get("title", "")
        words = re.findall(r"[A-Za-z]{3,}", title)
        for w in words:
            wl = w.lower()
            if wl not in stopwords:
                word_counts[wl] += 1

    # Pick top keyword(s) that appear in multiple drafts
    top_words = [w for w, c in word_counts.most_common(3) if c >= 2]
    if not top_words:
        top_words = [w for w, _ in word_counts.most_common(1)]

    if top_words:
        topic = " ".join(w.capitalize() for w in top_words[:2])
        name = f"{org_label} — {topic}"
    else:
        name = org_label
    # Truncate if too long for display
    return name if len(name) <= 50 else name[:47] + "…"
