# 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",
})


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) ===

_SALIENCE_KEYWORDS = {
    "critical": (["never", "always", "must not", "crashes", "data loss", "security",
                  "corruption", "deadlock", "race condition", "destroys", "silent fail",
                  "zero-day", "vulnerability", "irreversible"], 7.0),
    "actionable": (["fix", "use instead", "switch to", "replace with", "root cause",
                    "because", "prevents", "caused by", "broke", "workaround",
                    "migrate", "upgrade", "downgrade", "revert"], 4.0),
    "standard": (["should", "prefer", "better to", "works", "confirmed",
                  "discovered", "learned", "tested", "verified", "refactored",
                  "improved", "reduced"], 2.0),
}


def compute_item_salience(description: str, priority: float = 1.0,
                          emotional_intensity: float = 0.0) -> float:
    """Content-aware salience scoring based on keyword tiers.

    Replaces the old priority*emul formula that clustered 83% of entries at 2.0.
    Tiers: critical=7.0, actionable=4.0, standard=2.0, ambient=1.0.
    Boosts: priority (0-4.5 for priority 1-10), emotional (0-1.5), specificity (0-0.5).
    Range: 1.0-10.0.

    Note: _compute_priority() returns 1-10. At priority=5 (default), boost=2.0.
    At priority=7 (high), boost=3.0. At priority=10 (max), boost=4.5.
    """
    desc_lower = description.lower()
    base = 1.0  # ambient default
    for _tier_name, (keywords, tier_base) in _SALIENCE_KEYWORDS.items():
        if any(kw in desc_lower for kw in keywords):
            base = tier_base
            break
    priority_boost = max(0, (priority - 1)) * 0.5  # 0-4.5 for priority 1-10
    emotional_boost = emotional_intensity * 1.5      # 0-1.5 for intensity 0-1
    word_count = len(description.split())
    specificity_boost = min(0.5, word_count / 40.0)  # longer = more specific

    # Phase 4+: Negativity bias — negative-valence items get safety boost
    valence_boost = 0.0
    try:
        from pulse.core.valence import compute_valence
        valence, _ = compute_valence(description)
        if valence < -0.3:
            valence_boost = 0.5  # Negative content gets safety salience boost
    except Exception:
        pass

    return min(10.0, round(base + priority_boost + emotional_boost + specificity_boost + valence_boost, 1))


def compute_item_confidence(description: str, source_type: str = "bridge") -> float:
    """Content-aware initial confidence based on extraction quality.

    Continuous formula replacing the 3-tier step function that clustered
    97% of values at 0.45/0.50/0.55.  Now produces a smooth distribution
    across 0.30-0.90 using: base + length curve + specificity signals.
    """
    import math
    # Wider base range: bridge starts lower (0.35) to allow more upward spread
    base = {"git": 0.85, "manual": 0.70, "scribe": 0.45, "bridge": 0.35}.get(source_type, 0.30)
    desc_lower = description.lower()
    words = description.split()
    word_count = len(words)

    # Length curve: log growth 0 to ~0.20 (saturates ~80 words, wider range)
    length_boost = min(0.20, 0.20 * math.log1p(word_count) / math.log1p(80))

    # Vocabulary richness: unique/total ratio (0 to 0.08)
    unique_ratio = len(set(w.lower() for w in words)) / max(word_count, 1)
    richness_boost = 0.08 * unique_ratio

    # Specificity: domain keywords, failure signals, technical terms (wider boost)
    _FAILURE_WORDS = {"crash", "error", "fail", "broke", "bug", "timeout", "exception", "traceback"}
    _DOMAIN_WORDS = {"architecture", "security", "migration", "database", "schema",
                     "encryption", "sqlite", "api", "daemon", "pipeline", "deployment"}
    _TECHNICAL_WORDS = {"function", "class", "module", "import", "config", "parameter",
                        "threshold", "index", "query", "table", "column"}
    desc_words = set(desc_lower.split())
    failure_hits = len(desc_words & _FAILURE_WORDS)
    domain_hits = len(desc_words & _DOMAIN_WORDS)
    tech_hits = len(desc_words & _TECHNICAL_WORDS)
    specificity_boost = min(0.18, 0.04 * failure_hits + 0.04 * domain_hits + 0.02 * tech_hits)

    # Concrete references: file paths, numbers, function names (wider boost)
    import re
    has_path = 1 if re.search(r'[\w/\\]+\.\w{1,4}', description) else 0
    has_number = 1 if re.search(r'\b\d{2,}\b', description) else 0
    has_code = 1 if re.search(r'[a-z_]+\([^)]*\)', description) else 0
    concreteness_boost = 0.04 * has_path + 0.02 * has_number + 0.03 * has_code

    # Sentence structure: complete sentences get a bonus
    has_period = 1 if description.rstrip().endswith(('.', '!', '?')) else 0
    structure_boost = 0.03 * has_period

    total = base + length_boost + richness_boost + specificity_boost + concreteness_boost + structure_boost
    return min(0.90, max(0.25, round(total, 3)))

# ---------------------------------------------------------------------------
# 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,
)
