# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
Brain Analysis: Text similarity, counting, and neuroscience helpers.

Split from brain_utils.py for Law 7 compliance.
"""

from pathlib import Path


# === TEXT UTILITIES ===

_STOP_WORDS = frozenset({
    "the", "a", "an", "is", "in", "to", "of", "and", "for", "on", "it",
    "this", "that", "with", "from", "be", "was", "were", "are", "been",
    "has", "have", "had", "do", "did", "does", "will", "would", "could",
    "should", "can", "may", "we", "you", "i", "my", "our", "your", "me",
    "what", "how", "why", "when", "where", "which", "who", "not", "no",
    "but", "or", "if", "so", "at", "by", "up", "out", "about", "into",
    "just", "also", "than", "then", "its", "let", "lets",
})


import re

_SENTENCE_SPLIT = re.compile(r'(?<=[.!?])\s+')


def truncate_at_sentence(text: str, max_chars: int = 500) -> str:
    """Truncate text at the last sentence boundary before max_chars.

    If no sentence boundary found, falls back to word boundary.
    Never returns less than 40 chars (avoids over-truncation).
    """
    if not text or len(text) <= max_chars:
        return text.strip()

    candidate = text[:max_chars]
    sentences = _SENTENCE_SPLIT.split(candidate)
    if len(sentences) > 1:
        result = ""
        for s in sentences:
            s_stripped = s.strip()
            # Only include complete sentences (end with terminal punctuation)
            if not s_stripped or not s_stripped[-1] in '.!?)':
                break
            if len(result) + len(s_stripped) + 1 <= max_chars:
                result = (result + " " + s_stripped).strip() if result else s_stripped
            else:
                break
        if len(result) >= 40:
            return result.strip()

    for i in range(min(len(text), max_chars) - 1, max(39, max_chars // 2), -1):
        if text[i] in ".!?)":
            return text[:i + 1].strip()

    space_idx = text.rfind(" ", 40, max_chars)
    if space_idx > 0:
        return text[:space_idx].strip()

    return text[:max_chars].strip()


def text_similarity(a: str, b: str) -> float:
    """Simple word-overlap Jaccard similarity (0.0-1.0).

    Stop words are filtered from both inputs before computing overlap.
    Falls back to unfiltered if filtering removes all words from either side.
    """
    if not a or not b:
        return 0.0
    wa_raw = set(a.lower().split())
    wb_raw = set(b.lower().split())
    wa = wa_raw - _STOP_WORDS
    wb = wb_raw - _STOP_WORDS
    # Safety fallback: if filtering wiped either side, use unfiltered sets
    if not wa:
        wa = wa_raw
    if not wb:
        wb = wb_raw
    if not wa or not wb:
        return 0.0
    return len(wa & wb) / len(wa | wb)


def search_relevance(query: str, document: str) -> float:
    """Query-coverage similarity for search (0.0-1.0).

    Returns fraction of query words found in document.
    Unlike Jaccard, long documents don't dilute the score.
    Stop words are filtered from both query and document before computing
    overlap. Falls back to unfiltered if filtering removes all query words.
    """
    if not query or not document:
        return 0.0
    qw_raw = set(query.lower().split())
    dw_raw = set(document.lower().split())
    qw = qw_raw - _STOP_WORDS
    dw = dw_raw - _STOP_WORDS
    # Safety fallback: if filtering wiped the query, use unfiltered sets
    if not qw:
        qw = qw_raw
        dw = dw_raw
    if not qw:
        return 0.0
    return len(qw & dw) / len(qw)


def extract_domain(filename: str) -> str:
    """Extract domain name from evidence filename."""
    name = filename.replace(".json", "")
    parts = name.split("_")
    return parts[0] if parts else "unknown"



# === CONTENT-AWARE SALIENCE & CONFIDENCE (Phase 0 — Salience Calibration) ===






# ---------------------------------------------------------------------------
# Re-exports from split modules (preserves public API)
# ---------------------------------------------------------------------------
from .brain_analysis_counting import (  # noqa: F401
    count_lines, count_json_items, count_md_sections,
)
from .brain_analysis_neuro import (  # noqa: F401
    compute_weighted_count, compute_confidence_decay,
    detect_cross_domain_patterns, get_brain_density,
)
from .brain_analysis_scoring import (  # noqa: F401
    compute_item_salience, compute_item_confidence,
)
