"""Wiki generation from community structure.

Generates markdown pages for each detected community and an index page,
providing a navigable documentation wiki for the codebase architecture.
"""

from __future__ import annotations

import logging
import re
import sqlite3
from collections import Counter
from pathlib import Path
from typing import Any

from .communities import get_communities
from .flows import get_flows
from .graph import GraphStore, _sanitize_name

logger = logging.getLogger(__name__)


def _slugify(name: str) -> str:
    """Convert a community name to a safe filename slug."""
    slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
    return slug[:80] or "unnamed"


def _generate_community_page(store: GraphStore, community: dict[str, Any]) -> str:
    """Build markdown content for a single community.

    Includes: heading, overview (size, cohesion, language), members table
    (top 50), execution flows through the community, and dependencies.

    Args:
        store: The graph store.
        community: Community dict from get_communities().

    Returns:
        Markdown string for the community page.
    """
    name = community["name"]
    size = community["size"]
    cohesion = community.get("cohesion", 0.0)
    lang = community.get("dominant_language", "")
    description = community.get("description", "")

    lines: list[str] = []
    lines.append(f"# {name}")
    lines.append("")

    # Overview section
    lines.append("## Overview")
    lines.append("")
    if description:
        lines.append(f"{description}")
        lines.append("")
    lines.append(f"- **Size**: {size} nodes")
    lines.append(f"- **Cohesion**: {cohesion:.4f}")
    if lang:
        lines.append(f"- **Dominant Language**: {lang}")
    lines.append("")

    # Members table (top 50)
    member_qns = community.get("members", [])
    lines.append("## Members")
    lines.append("")
    if member_qns:
        lines.append("| Name | Kind | File | Lines |")
        lines.append("|------|------|------|-------|")

        # Fetch node details for members (limit to 50)
        member_count = 0
        for qn in member_qns[:50]:
            node = store.get_node(qn)
            if node and node.kind != "File":
                node_name = _sanitize_name(node.name)
                lines.append(
                    f"| {node_name} | {node.kind} | {node.file_path} "
                    f"| {node.line_start}-{node.line_end} |"
                )
                member_count += 1

        if not member_count:
            # Remove the table headers if no members were added
            lines.pop()  # header separator
            lines.pop()  # header
            lines.append("No non-file members found.")

        if len(member_qns) > 50:
            lines.append("")
            lines.append(f"*... and {len(member_qns) - 50} more members.*")
    else:
        lines.append("No members found.")
    lines.append("")

    # Execution flows through community
    lines.append("## Execution Flows")
    lines.append("")
    member_set = set(member_qns)
    try:
        all_flows = get_flows(store, sort_by="criticality", limit=200)
        community_flows: list[dict] = []
        for flow in all_flows:
            # Check if this flow passes through any community member
            flow_qns = store.get_flow_qualified_names(flow["id"])
            if flow_qns & member_set:
                community_flows.append(flow)

        if community_flows:
            for flow in community_flows[:10]:
                flow_name = _sanitize_name(flow.get("name", "unnamed"))
                criticality = flow.get("criticality", 0.0)
                depth = flow.get("depth", 0)
                lines.append(
                    f"- **{flow_name}** (criticality: {criticality:.2f}, depth: {depth})"
                )
            if len(community_flows) > 10:
                lines.append(f"- *... and {len(community_flows) - 10} more flows.*")
        else:
            lines.append("No execution flows pass through this community.")
    except sqlite3.OperationalError as exc:
        logger.debug("wiki: flows table unavailable: %s", exc)
        lines.append("Execution flow data not available.")
    lines.append("")

    # Dependencies (cross-community edges)
    lines.append("## Dependencies")
    lines.append("")
    try:
        outgoing_targets: Counter[str] = Counter()
        incoming_sources: Counter[str] = Counter()
        if member_qns:
            qns = list(member_qns)

            # Outgoing: source is a member
            for t in store.get_outgoing_targets(qns):
                if t not in member_set:
                    outgoing_targets[t] += 1

            # Incoming: target is a member
            for s in store.get_incoming_sources(qns):
                if s not in member_set:
                    incoming_sources[s] += 1

        if outgoing_targets:
            lines.append("### Outgoing")
            lines.append("")
            for target, count in outgoing_targets.most_common(15):
                lines.append(f"- `{_sanitize_name(target)}` ({count} edge(s))")
            lines.append("")

        if incoming_sources:
            lines.append("### Incoming")
            lines.append("")
            for source, count in incoming_sources.most_common(15):
                lines.append(f"- `{_sanitize_name(source)}` ({count} edge(s))")
            lines.append("")

        if not outgoing_targets and not incoming_sources:
            lines.append("No cross-community dependencies detected.")
            lines.append("")
    except sqlite3.OperationalError as exc:
        logger.debug("wiki: dependency edges unavailable: %s", exc)
        lines.append("Dependency data not available.")
        lines.append("")

    return "\n".join(lines)


def generate_wiki(
    store: GraphStore,
    wiki_dir: str | Path,
    force: bool = False,
) -> dict[str, Any]:
    """Generate a markdown wiki from the community structure.

    For each community, generates a markdown page. Also generates an
    index.md with links to all community pages.

    Args:
        store: The graph store.
        wiki_dir: Directory to write wiki pages into.
        force: If True, regenerate all pages even if content unchanged.

    Returns:
        Dict with pages_generated, pages_updated, pages_unchanged counts.
    """
    wiki_path = Path(wiki_dir)
    wiki_path.mkdir(parents=True, exist_ok=True)

    communities = get_communities(store)

    pages_generated = 0
    pages_updated = 0
    pages_unchanged = 0

    page_entries: list[tuple[str, str, int]] = []  # (slug, name, size)

    # Track slugs we've already used in THIS run so two communities that
    # slugify to the same filename don't overwrite each other (#222 follow-up).
    # Previously "Data Processing" and "data processing" both became
    # "data-processing.md", causing silent data loss and inflated "updated"
    # counters (each collision was counted as an update while only one file
    # made it to disk).
    used_slugs: set[str] = set()

    for comm in communities:
        name = comm["name"]
        base_slug = _slugify(name)
        slug = base_slug
        suffix = 2
        while slug in used_slugs:
            slug = f"{base_slug}-{suffix}"
            suffix += 1
        used_slugs.add(slug)

        filename = f"{slug}.md"
        filepath = wiki_path / filename

        content = _generate_community_page(store, comm)

        if filepath.exists() and not force:
            existing = filepath.read_text(encoding="utf-8", errors="replace")
            if existing == content:
                pages_unchanged += 1
                page_entries.append((slug, name, comm["size"]))
                continue

        already_existed = filepath.exists()
        filepath.write_text(content, encoding="utf-8")
        if already_existed:
            pages_updated += 1
        else:
            pages_generated += 1
        page_entries.append((slug, name, comm["size"]))

    # Generate index.md
    index_lines: list[str] = []
    index_lines.append("# Code Wiki")
    index_lines.append("")
    index_lines.append(
        "Auto-generated documentation from the code knowledge graph community structure."
    )
    index_lines.append("")
    index_lines.append(f"**Total communities**: {len(communities)}")
    index_lines.append("")
    index_lines.append("## Communities")
    index_lines.append("")
    index_lines.append("| Community | Size | Link |")
    index_lines.append("|-----------|------|------|")
    for slug, name, size in sorted(page_entries, key=lambda x: x[1]):
        index_lines.append(f"| {name} | {size} | [{slug}.md]({slug}.md) |")
    index_lines.append("")

    index_content = "\n".join(index_lines)
    index_path = wiki_path / "index.md"

    if index_path.exists() and not force:
        existing_index = index_path.read_text(encoding="utf-8", errors="replace")
        if existing_index == index_content:
            pages_unchanged += 1
        else:
            index_path.write_text(index_content, encoding="utf-8")
            pages_updated += 1
    else:
        index_path.write_text(index_content, encoding="utf-8")
        pages_generated += 1

    return {
        "pages_generated": pages_generated,
        "pages_updated": pages_updated,
        "pages_unchanged": pages_unchanged,
    }


def get_wiki_page(wiki_dir: str | Path, page_name: str) -> str | None:
    """Retrieve a specific wiki page by community name.

    Args:
        wiki_dir: Directory containing wiki pages.
        page_name: Community name (will be slugified for filename lookup).

    Returns:
        Page content as a string, or None if the page does not exist.
    """
    wiki_path = Path(wiki_dir)
    slug = _slugify(page_name)
    filepath = wiki_path / f"{slug}.md"

    if filepath.is_file():
        return filepath.read_text(encoding="utf-8", errors="replace")

    # Fallback: try exact filename match — with path traversal protection
    exact_path = (wiki_path / page_name).resolve()
    if exact_path.is_file() and exact_path.is_relative_to(wiki_path.resolve()):
        return exact_path.read_text(encoding="utf-8", errors="replace")

    # Fallback: search for partial match
    if wiki_path.is_dir():
        for p in wiki_path.iterdir():
            if p.suffix == ".md" and slug in p.stem:
                return p.read_text(encoding="utf-8", errors="replace")

    return None
