"""Deterministic document analysis for docsctx."""

from __future__ import annotations

import re
from collections import Counter, defaultdict, deque
from dataclasses import dataclass

STOPWORDS = {
    "about",
    "after",
    "also",
    "and",
    "are",
    "because",
    "been",
    "but",
    "can",
    "does",
    "each",
    "from",
    "have",
    "into",
    "more",
    "most",
    "not",
    "only",
    "other",
    "that",
    "their",
    "them",
    "then",
    "there",
    "these",
    "this",
    "those",
    "through",
    "with",
    "your",
}

WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9_-]{2,}")
SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+")


@dataclass(slots=True)
class AnalyzedDocument:
    document_id: int
    title: str
    source_uri: str
    doc_type: str
    structure: dict[str, object]
    entities: list[tuple[str, int]]
    relationships: list[tuple[str, str, int]]
    claims: list[tuple[str, str, str]] = None


@dataclass(slots=True)
class CommunityResult:
    label: str
    summary: str
    top_terms: list[str]
    document_ids: list[int]


def analyze_document(document_id: int, title: str, source_uri: str, doc_type: str, content: str) -> AnalyzedDocument:
    headings = _extract_headings(content)
    summary_sentences = SENTENCE_SPLIT_RE.split(content.strip()) if content.strip() else []
    summary = " ".join(summary_sentences[:2]).strip()
    terms = _top_terms(content, limit=8)
    relationships = _relationships_for_terms(terms)
    structure = {
        "title": title,
        "source_uri": source_uri,
        "doc_type": doc_type,
        "summary": summary,
        "headings": headings,
        "sections": len(headings),
        "top_terms": [term for term, _ in terms],
    }
    return AnalyzedDocument(
        document_id=document_id,
        title=title,
        source_uri=source_uri,
        doc_type=doc_type,
        structure=structure,
        entities=terms,
        relationships=relationships,
        claims=[],
    )


def build_communities(documents: list[AnalyzedDocument], max_levels: int = 3) -> list[CommunityResult]:
    """Build communities. Uses Louvain if networkx is installed, falls back to BFS clustering."""
    try:
        import networkx as nx  # noqa: F401
        from networkx.algorithms.community import louvain_communities  # noqa: F401

        return _build_communities_louvain(documents, nx, louvain_communities)
    except ImportError:
        return _build_communities_bfs(documents)


def _build_communities_louvain(documents: list[AnalyzedDocument], nx, louvain_communities) -> list[CommunityResult]:
    """Louvain modularity-based community detection using networkx."""
    if not documents:
        return []

    by_id = {doc.document_id: doc for doc in documents}
    term_sets = {doc.document_id: {term for term, _ in doc.entities[:5]} for doc in documents}

    G = nx.Graph()
    for doc in documents:
        G.add_node(doc.document_id)

    doc_list = list(documents)
    for i, left in enumerate(doc_list):
        for right in doc_list[i + 1 :]:
            overlap = len(term_sets[left.document_id] & term_sets[right.document_id])
            if overlap > 0:
                G.add_edge(left.document_id, right.document_id, weight=overlap)

    if G.number_of_edges() == 0:
        return [
            CommunityResult(
                label=by_id[doc.document_id].doc_type,
                summary=f"Document: {by_id[doc.document_id].title}",
                top_terms=[t for t, _ in by_id[doc.document_id].entities[:3]],
                document_ids=[doc.document_id],
            )
            for doc in documents
        ]

    partition = louvain_communities(G, weight="weight", seed=42)

    communities: list[CommunityResult] = []
    for node_set in partition:
        members = sorted(node_set)
        aggregate_terms: Counter[str] = Counter()
        for doc_id in members:
            for term, count in by_id[doc_id].entities[:5]:
                aggregate_terms[term] += count
        top_terms = [t for t, _ in aggregate_terms.most_common(3)] or [by_id[members[0]].doc_type]
        label = ", ".join(top_terms)
        member_titles = ", ".join(by_id[m].title for m in members[:4])
        summary = f"Documents clustered by modularity: {label}. Members: {member_titles}."
        communities.append(
            CommunityResult(
                label=label,
                summary=summary,
                top_terms=top_terms,
                document_ids=members,
            )
        )

    communities.sort(key=lambda item: (-len(item.document_ids), item.label))
    return communities


def _build_communities_bfs(documents: list[AnalyzedDocument]) -> list[CommunityResult]:
    """BFS-based community detection (fallback when networkx unavailable)."""
    if not documents:
        return []

    term_sets = {doc.document_id: {term for term, _ in doc.entities[:5]} for doc in documents}
    adjacency: dict[int, set[int]] = defaultdict(set)
    for index, left in enumerate(documents):
        for right in documents[index + 1 :]:
            overlap = term_sets[left.document_id] & term_sets[right.document_id]
            if overlap:
                adjacency[left.document_id].add(right.document_id)
                adjacency[right.document_id].add(left.document_id)

    communities: list[CommunityResult] = []
    visited: set[int] = set()
    by_id = {doc.document_id: doc for doc in documents}

    for document in documents:
        if document.document_id in visited:
            continue
        queue: deque[int] = deque([document.document_id])
        members: list[int] = []
        aggregate_terms: Counter[str] = Counter()
        while queue:
            current = queue.popleft()
            if current in visited:
                continue
            visited.add(current)
            members.append(current)
            for term, count in by_id[current].entities[:5]:
                aggregate_terms[term] += count
            for neighbor in adjacency[current]:
                if neighbor not in visited:
                    queue.append(neighbor)

        top_terms = [term for term, _ in aggregate_terms.most_common(3)] or [by_id[members[0]].doc_type]
        label = ", ".join(top_terms)
        member_titles = ", ".join(by_id[member].title for member in members[:4])
        summary = f"Documents clustered by shared terms: {label}. Members: {member_titles}."
        communities.append(
            CommunityResult(
                label=label,
                summary=summary,
                top_terms=top_terms,
                document_ids=sorted(members),
            )
        )

    communities.sort(key=lambda item: (-len(item.document_ids), item.label))
    return communities


def _extract_headings(content: str) -> list[str]:
    headings: list[str] = []
    for line in content.splitlines():
        stripped = line.strip()
        if not stripped:
            continue
        if len(stripped) <= 90 and (stripped == stripped.title() or stripped.endswith(":")):
            headings.append(stripped.rstrip(":"))
    return headings[:12]


def _top_terms(content: str, limit: int = 8) -> list[tuple[str, int]]:
    counts: Counter[str] = Counter()
    for match in WORD_RE.finditer(content.lower()):
        word = match.group(0)
        if word in STOPWORDS or word.isdigit() or len(word) < 4:
            continue
        counts[word] += 1
    return counts.most_common(limit)


def _relationships_for_terms(terms: list[tuple[str, int]]) -> list[tuple[str, str, int]]:
    top = terms[:4]
    relationships: list[tuple[str, str, int]] = []
    for index, (left, left_count) in enumerate(top):
        for right, right_count in top[index + 1 :]:
            relationships.append((left, right, min(left_count, right_count)))
    return relationships
