"""Deterministic lexical evidence selection; excerpts are never stitched together."""
from __future__ import annotations

import io
import re
import tokenize
from collections import Counter

STOP_WORDS = frozenset({"a", "an", "and", "are", "as", "at", "be", "by", "code", "def", "do", "does", "for", "from", "function", "get", "how", "in", "into", "is", "it", "method", "of", "on", "one", "onto", "or", "return", "self", "test", "tests", "the", "this", "to", "use", "value", "with"})
_EXCLUSION = re.compile(r"\b(?:without|excluding|exclude|except|avoid)\b", re.IGNORECASE)
_EXCLUSION_END = re.compile(r"[,;.!?]|\b(?:but|instead|rather|while)\b", re.IGNORECASE)
_EXCLUSION_MODIFIERS = frozenset({"unrelated", "irrelevant", "unwanted", "other"})
_BEHAVIOR_LEAD = re.compile(
    r"\s*(?:leaking|buffering|mutating|losing|following|overriding|accepting|"
    r"converting|forcing|dropping|creating|reading|writing|executing|downloading|blocking)\b",
    re.IGNORECASE,
)


def terms(text, fold=True):
    """Query or evidence words; ``fold`` cuts a plural "s", so both sides must use the same rule."""
    text = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", text)
    words = re.findall(r"[a-zA-Z][a-zA-Z0-9]*", text.lower())
    return {word[:-1] if fold and word.endswith("s") and not word.endswith("ss") and len(word) > 4 else word
            for word in words if len(word) > 2 and word not in STOP_WORDS}


def task_terms(text: str, fold: bool = True) -> tuple[set[str], set[str]]:
    """Separate explicit exclusions from positive task terms.

    The negative set is a *ranking hint*, never a hard filter. Deliberately
    recognize only explicit exclusion words: ordinary negation such as
    ``not found`` often describes the bug rather than irrelevant code.
    """
    positive_parts: list[str] = []
    negative_parts: list[str] = []
    remaining = text
    while match := _EXCLUSION.search(remaining):
        positive_parts.append(remaining[:match.start()])
        tail = remaining[match.end():]
        end = _EXCLUSION_END.search(tail)
        if end is None:
            negative_parts.append(tail)
            remaining = ""
            break
        negative_parts.append(tail[:end.start()])
        remaining = tail[end.end():]
    positive_parts.append(remaining)
    positive = terms(" ".join(positive_parts), fold) - _EXCLUSION_MODIFIERS
    negative = terms(" ".join(negative_parts), fold) - _EXCLUSION_MODIFIERS
    return positive, negative


def scope_exclusion_terms(text: str, fold: bool = True) -> set[str]:
    """Soft source penalties, without penalizing recognized behavioral clauses.

    Keep legacy positive retrieval terms and excerpt selection unchanged. Only
    high-confidence action leads after ``without``/``avoid`` are neutral here;
    ambiguous nouns and explicit scopes retain their existing interpretation.
    This is a bounded lexical rule, not a general natural-language parser.
    """
    matches = list(_EXCLUSION.finditer(text))

    def behavior(match):
        return match.group().lower() in {"without", "avoid"} and _BEHAVIOR_LEAD.match(text[match.end():])

    if not any(behavior(match) for match in matches):
        return task_terms(text, fold)[1]
    negative_parts = []
    for index, match in enumerate(matches):
        if behavior(match):
            continue
        tail = text[match.end():matches[index + 1].start() if index + 1 < len(matches) else len(text)]
        end = _EXCLUSION_END.search(tail)
        negative_parts.append(tail[:end.start()] if end else tail)
    return terms(" ".join(negative_parts), fold) - _EXCLUSION_MODIFIERS


def file_excerpt(lines: list[str], path: str, query: str) -> str:
    """Query-focused file evidence for a shortlist decision.

    The product reranker and the eval trials show a model this same text. It
    has up to two numbered windows around the lines with the most task terms.
    """
    if not lines:
        return f"File: {path}\n(empty file)"
    positive, _ = task_terms(query)
    centers: list[int] = []
    scored = sorted(((len(terms(line) & positive), -index, index)
                     for index, line in enumerate(lines)), reverse=True)
    for overlap, _order, index in scored:
        if overlap == 0 and centers:
            break
        if all(abs(index - old) > 24 for old in centers):
            centers.append(index)
        if len(centers) == 2:
            break
    windows = []
    for center in sorted(centers or [0]):
        start, end = max(0, center - 8), min(len(lines), center + 9)
        windows.append("\n".join(f"{number + 1}: {lines[number]}"
                                 for number in range(start, end)))
    return (f"File: {path}\n" + "\n...\n".join(windows))[:1350]


def source_excerpts(source, definition, task_hint, *, preview_end=0):
    """Rank code identifiers, favor branches/assignments, and return disjoint ranges.

    This is a lexical aid, not semantic relevance or complete control-flow coverage.
    Python strings/comments do not become evidence just by repeating query words.
    """
    from attocode_intel.structural_evidence import structural_excerpts
    structured = structural_excerpts(source, definition, task_hint, preview_end)
    if structured is not None:
        return structured
    query, _ = task_terms(task_hint)
    if not query:
        return []
    first, last = definition.start_line, min(definition.end_line, len(source))
    identifiers = {}
    if definition.file_path.endswith(".py"):
        try:
            for token in tokenize.generate_tokens(io.StringIO("\n".join(source)).readline):
                if token.type == tokenize.NAME and first <= token.start[0] <= last:
                    identifiers.setdefault(token.start[0], []).append(token.string)
        except (tokenize.TokenError, IndentationError, SyntaxError):
            pass  # Only retain tokens actually read; do not invent coverage.
    else:
        for number in range(first, last + 1):
            line = source[number - 1].strip()
            if line.startswith(("#", "//", "/*", "*")):
                continue
            code = re.sub(r"(['\"]).*?\1", "", line).split("//", 1)[0]
            identifiers[number] = re.findall(r"[A-Za-z_][A-Za-z_0-9]*", code)
    candidates = []
    for number, names in identifiers.items():
        if number <= preview_end:
            continue  # The caller already supplies this source verbatim.
        hits = Counter(term for name in names for term in terms(name) if term in query)
        if not hits:
            continue
        code = source[number - 1].strip()
        # Guards and stores often express behavior more directly than annotations.
        action = bool(re.match(r"(?:if|elif|else|return|case|throw|raise)\b", code) or
                      (not code.endswith(",") and re.search(r"(?<![=!<>])=(?!=)", code)))
        # Multiple participating identifiers help distinguish access from simple
        # initialization, but repetition is capped so annotations cannot dominate.
        score = sum(min(count, 2) for count in hits.values()) + 2 * int(action)
        candidates.append((number, score, set(hits)))
    selected, covered = [], set()
    while candidates and len(selected) < 3:
        number, _, hits = max(candidates, key=lambda row: (row[1] + len(row[2] - covered), -row[0]))
        start, end = max(first, preview_end + 1, number - 1), min(last, number + 3)
        selected.append({"file_path": definition.file_path, "start_line": start, "end_line": end,
                         "text": "\n".join(source[start - 1:end]), "matched_terms": sorted(hits), "context_ranges": [],
                         "context_incomplete": True, "context_relation": "structural context unavailable"})
        covered.update(hits)
        candidates = [row for row in candidates if min(last, row[0] + 3) < start or max(first, row[0] - 1) > end]
    # Keep relevance ordering, so output budgeting drops lower ranked whole excerpts.
    return selected
