"""
Text analysis utilities for attribution.

Shared keyword extraction, line search, and snippet utilities
used by all analyzers.
"""

from __future__ import annotations

import re
from typing import List, Tuple

STOP_WORDS = frozenset({
    "a", "an", "and", "are", "as", "at", "be", "but", "by", "do",
    "for", "from", "get", "had", "has", "have", "he", "her", "him",
    "his", "how", "if", "in", "into", "is", "it", "its", "let",
    "may", "my", "no", "nor", "not", "of", "on", "or", "our",
    "own", "per", "set", "she", "so", "the", "to", "too", "up",
    "us", "use", "was", "we", "who", "why", "with", "you", "your",
    "this", "that", "then", "than", "them", "they", "what", "when",
    "will", "can", "did", "does", "been", "each", "just", "more",
    "some", "such", "very", "also",
})


def extract_keywords(text: str, min_length: int = 3) -> List[str]:
    """
    Extract meaningful words from text.

    Filters out stop words, removes punctuation, lowercases.
    """
    words = re.findall(r"[a-zA-Z0-9]+", text.lower())
    return [
        w for w in words
        if len(w) >= min_length and w not in STOP_WORDS
    ]


def find_keyword_in_lines(
    content: str, keywords: List[str]
) -> List[Tuple[int, str, List[str]]]:
    """
    Search multi-line content for keywords using word-boundary matching.

    Returns list of (line_number, line_text, matched_keywords).
    Line numbers are 1-based.
    """
    results = []
    lines = content.split("\n")
    # Pre-compile word-boundary patterns for each keyword
    keyword_patterns = [
        (k.lower(), re.compile(r"\b" + re.escape(k.lower()) + r"\b"))
        for k in keywords
    ]

    for i, line in enumerate(lines, start=1):
        line_lower = line.lower()
        matched = [k for k, pat in keyword_patterns if pat.search(line_lower)]
        if matched:
            results.append((i, line, matched))

    return results


def extract_context_lines(
    content: str, line_number: int, context_size: int = 2
) -> str:
    """
    Get surrounding lines around target line.

    Args:
        content: Multi-line text.
        line_number: 1-based line number.
        context_size: Lines above and below to include.

    Returns:
        Formatted string with line numbers.
    """
    lines = content.split("\n")
    idx = line_number - 1  # Convert to 0-based
    start = max(0, idx - context_size)
    end = min(len(lines), idx + context_size + 1)

    result_lines = []
    for i in range(start, end):
        prefix = ">>>" if i == idx else "   "
        result_lines.append(f"{prefix} {i + 1}: {lines[i]}")

    return "\n".join(result_lines)


def truncate_snippet(text: str, max_length: int = 200) -> str:
    """
    Truncate long text at word boundaries.

    Adds '...' if truncated.
    """
    if len(text) <= max_length:
        return text

    truncated = text[:max_length]
    last_space = truncated.rfind(" ")
    if last_space > max_length // 2:
        truncated = truncated[:last_space]

    return truncated.rstrip() + "..."


def compute_keyword_density(text: str, keywords: List[str]) -> float:
    """
    Measure how many keywords appear in text using word-boundary matching.

    Returns ratio: matched_keywords / total_keywords.
    Returns 0.0 if keywords is empty.
    """
    if not keywords:
        return 0.0

    text_lower = text.lower()
    matched = sum(
        1 for k in keywords
        if re.search(r"\b" + re.escape(k.lower()) + r"\b", text_lower)
    )
    return matched / len(keywords)
