"""Documentation synthesis from completed issue history.

Synthesizes architecture documentation from completed issues by scoring
relevance to a given topic, ordering chronologically by completion date,
and constructing a structured markdown document.
"""

from __future__ import annotations

import re
from datetime import date
from pathlib import Path

from little_loops.issue_history.models import CompletedIssue
from little_loops.text_utils import extract_words, score_bm25


def score_relevance(
    topic: str,
    issue: CompletedIssue,
    content: str,
    corpus_stats: dict | None = None,
    scoring: str = "intersection",
) -> float:
    """Score how relevant a completed issue is to a topic.

    Supports three scoring modes:
    - "intersection" (default): fraction of topic words appearing in the issue.
      Works well for topic matching because it doesn't penalize long documents.
    - "hybrid": intersection * 0.5 + normalized BM25 * 0.5. Requires corpus_stats.
    - "bm25": normalized BM25 score only. Requires corpus_stats.

    Args:
        topic: Search topic string
        issue: Completed issue to score
        content: Raw file content of the issue
        corpus_stats: Corpus statistics for BM25 (doc_freq, avg_doc_len, total_docs).
            Required for "hybrid" and "bm25" modes; ignored for "intersection".
        scoring: Scoring mode — "intersection", "hybrid", or "bm25"

    Returns:
        Relevance score from 0.0 to 1.0
    """
    topic_words = extract_words(topic)
    if not topic_words:
        return 0.0

    # Combine issue ID, filename stem, and content for matching
    issue_text = f"{issue.issue_id} {issue.path.stem.replace('-', ' ')} {content}"
    issue_words = extract_words(issue_text)

    # Intersection-based: what fraction of topic words appear in the issue?
    intersection_score = len(topic_words & issue_words) / len(topic_words)

    if scoring == "intersection" or not corpus_stats:
        return intersection_score

    # BM25 scoring requires at least one matching term (early exit for efficiency)
    if intersection_score == 0.0:
        return 0.0

    raw_bm25 = score_bm25(topic_words, issue_words, **corpus_stats)
    # Normalize BM25 to [0, 1) using x / (x + 1) — smooth and monotonic
    bm25_normalized = raw_bm25 / (raw_bm25 + 1) if raw_bm25 > 0 else 0.0

    if scoring == "bm25":
        return bm25_normalized
    # "hybrid": equal blend of intersection (recall) and BM25 (precision)
    return intersection_score * 0.5 + bm25_normalized * 0.5


def _compute_corpus_stats(candidates: list[tuple[CompletedIssue, str]]) -> dict:
    """Compute BM25 corpus statistics over a set of candidate issues.

    Args:
        candidates: List of (issue, content) tuples

    Returns:
        dict with keys: doc_freq (dict[str, int]), avg_doc_len (float), total_docs (int)
    """
    doc_words_list: list[set[str]] = []
    for issue, content in candidates:
        issue_text = f"{issue.issue_id} {issue.path.stem.replace('-', ' ')} {content}"
        doc_words_list.append(extract_words(issue_text))

    total_docs = len(doc_words_list)
    if total_docs == 0:
        return {"doc_freq": {}, "avg_doc_len": 0.0, "total_docs": 0}

    doc_freq: dict[str, int] = {}
    total_len = 0
    for words in doc_words_list:
        total_len += len(words)
        for word in words:
            doc_freq[word] = doc_freq.get(word, 0) + 1

    return {
        "doc_freq": doc_freq,
        "avg_doc_len": total_len / total_docs,
        "total_docs": total_docs,
    }


def _extract_section(content: str, heading: str) -> str:
    """Extract content under a markdown heading.

    Args:
        content: Full markdown content
        heading: Heading text to find (without ##)

    Returns:
        Section content (empty string if not found)
    """
    pattern = rf"^##\s+{re.escape(heading)}\s*$"
    match = re.search(pattern, content, re.MULTILINE)
    if not match:
        return ""

    start = match.end()
    # Find next heading of same or higher level
    next_heading = re.search(r"^##\s", content[start:], re.MULTILINE)
    if next_heading:
        end = start + next_heading.start()
    else:
        end = len(content)

    return content[start:end].strip()


def _extract_title(content: str) -> str:
    """Extract the H1 title from issue content.

    Args:
        content: Issue file content

    Returns:
        Title text, or empty string if not found
    """
    match = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
    return match.group(1).strip() if match else ""


def synthesize_docs(
    topic: str,
    issues: list[CompletedIssue],
    contents: dict[Path, str],
    format: str = "narrative",
    min_relevance: float = 0.5,
    since: date | None = None,
    issue_type: str | None = None,
    scoring: str = "intersection",
) -> str:
    """Synthesize documentation from completed issues matching a topic.

    Filters issues by relevance, orders chronologically by completion date,
    and builds a markdown document.

    Args:
        topic: Topic to search for
        issues: List of completed issues
        contents: Pre-loaded issue file contents (path -> content)
        format: Output format - "narrative" or "structured"
        min_relevance: Minimum relevance score threshold
        since: Only include issues completed after this date
        issue_type: Filter by issue type (BUG, FEAT, ENH)
        scoring: Relevance scoring mode — "intersection" (default), "hybrid", or "bm25"

    Returns:
        Synthesized markdown document
    """
    # Pre-filter by type and date (before relevance scoring)
    candidates: list[tuple[CompletedIssue, str]] = []
    for issue in issues:
        content = contents.get(issue.path, "")
        if not content:
            continue
        if issue_type and issue.issue_type != issue_type:
            continue
        if since and issue.completed_date and issue.completed_date < since:
            continue
        candidates.append((issue, content))

    # Compute corpus statistics for BM25-based scoring modes
    corpus_stats: dict | None = None
    if scoring != "intersection" and candidates:
        corpus_stats = _compute_corpus_stats(candidates)

    # Score and filter issues
    scored: list[tuple[CompletedIssue, float, str]] = []
    for issue, content in candidates:
        score = score_relevance(topic, issue, content, corpus_stats=corpus_stats, scoring=scoring)
        if score >= min_relevance:
            scored.append((issue, score, content))

    # Sort by completion date ascending (oldest first), then by score
    scored.sort(
        key=lambda x: (
            x[0].completed_date or date.min,
            -x[1],
        )
    )

    if not scored:
        return f"No completed issues found matching topic: {topic}"

    if format == "structured":
        return build_structured_doc(topic, scored)
    return build_narrative_doc(topic, scored)


def build_narrative_doc(
    topic: str,
    scored_issues: list[tuple[CompletedIssue, float, str]],
) -> str:
    """Build a narrative-style documentation document.

    Each issue becomes a section describing what was built and why,
    ordered chronologically to read as a development narrative.

    Args:
        topic: The topic being documented
        scored_issues: List of (issue, score, content) tuples, pre-sorted

    Returns:
        Markdown document string
    """
    lines: list[str] = []

    lines.append(f"# {topic}")
    lines.append("")
    lines.append(
        f"*Synthesized from {len(scored_issues)} completed issue(s). Generated from issue history.*"
    )
    lines.append("")

    for issue, _score, content in scored_issues:
        title = _extract_title(content)
        display_title = title or issue.issue_id

        # Section heading with date
        date_str = issue.completed_date.isoformat() if issue.completed_date else "unknown"
        lines.append(f"## {display_title}")
        lines.append("")
        lines.append(
            f"*{issue.issue_id} | Completed: {date_str} | "
            f"Type: {issue.issue_type} | Priority: {issue.priority}*"
        )
        lines.append("")

        # Summary section
        summary = _extract_section(content, "Summary")
        if summary:
            lines.append(summary)
            lines.append("")

        # Motivation section
        motivation = _extract_section(content, "Motivation")
        if motivation:
            lines.append(f"**Motivation:** {motivation}")
            lines.append("")

        # Implementation notes
        impl_notes = _extract_section(content, "Implementation Notes")
        if not impl_notes:
            impl_notes = _extract_section(content, "Resolution")
        if impl_notes:
            lines.append(f"**Implementation:** {impl_notes}")
            lines.append("")

        lines.append("---")
        lines.append("")

    return "\n".join(lines)


def build_structured_doc(
    topic: str,
    scored_issues: list[tuple[CompletedIssue, float, str]],
) -> str:
    """Build a structured documentation document.

    Organized with a summary table followed by detailed sections,
    focusing on technical content rather than narrative flow.

    Args:
        topic: The topic being documented
        scored_issues: List of (issue, score, content) tuples, pre-sorted

    Returns:
        Markdown document string
    """
    lines: list[str] = []

    lines.append(f"# {topic}")
    lines.append("")
    lines.append(
        f"*Synthesized from {len(scored_issues)} completed issue(s). Generated from issue history.*"
    )
    lines.append("")

    # Summary table
    lines.append("## Overview")
    lines.append("")
    lines.append("| Issue | Type | Priority | Completed | Relevance |")
    lines.append("|-------|------|----------|-----------|-----------|")
    for issue, score, content in scored_issues:
        title = _extract_title(content) or issue.issue_id
        # Truncate long titles for the table
        if len(title) > 60:
            title = title[:57] + "..."
        date_str = issue.completed_date.isoformat() if issue.completed_date else "N/A"
        lines.append(
            f"| {title} | {issue.issue_type} | {issue.priority} | {date_str} | {score:.0%} |"
        )
    lines.append("")

    # Detailed sections
    lines.append("## Details")
    lines.append("")

    for issue, _score, content in scored_issues:
        title = _extract_title(content) or issue.issue_id
        date_str = issue.completed_date.isoformat() if issue.completed_date else "unknown"

        lines.append(f"### {title}")
        lines.append("")
        lines.append(f"**ID:** {issue.issue_id} | **Completed:** {date_str}")
        lines.append("")

        summary = _extract_section(content, "Summary")
        if summary:
            lines.append(summary)
            lines.append("")

        # Expected behavior or proposed solution
        expected = _extract_section(content, "Expected Behavior")
        if not expected:
            expected = _extract_section(content, "Proposed Solution")
        if expected:
            lines.append(f"**Solution:** {expected}")
            lines.append("")

        impl_notes = _extract_section(content, "Implementation Notes")
        if not impl_notes:
            impl_notes = _extract_section(content, "Resolution")
        if impl_notes:
            lines.append(f"**Implementation:** {impl_notes}")
            lines.append("")

    return "\n".join(lines)
