#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Extended scoring functions for brain search — P27/P28/P29/P30 neuroscience refinements.

Items housed here to keep search_scoring.py under 300 lines:
  - Item 113: Encoding specificity match
  - Item 114: Goal-directed attention allocation (urgency boost)
  - Item 115: Semantic satiation — repeated concept penalty
  - Item 118: Contextual inhibition — suppress irrelevant domains
  - Item 119: Memory strength vs accessibility distinction
  - Item 124: Surprise-based attention capture (P29)
  - Item 126: Cortical column organization (P30)
"""
import logging
import re
import statistics
from collections import Counter
from typing import Dict, List, Any, Optional

log = logging.getLogger("pulse.brain_search.search_scoring_ext")

# ---------------------------------------------------------------------------
# Item 113: Encoding specificity match (Memory)
# ---------------------------------------------------------------------------
_ENCODING_BONUS = 0.1


def _apply_encoding_specificity(item: dict, current_task_type: Optional[str]) -> dict:
    """Boost items whose creation context matches the current retrieval context."""
    try:
        if not current_task_type:
            return item
        created_task_type = item.get("metadata", {}).get("created_task_type", "") or item.get(
            "created_task_type", ""
        )
        if not created_task_type:
            tags = item.get("tags", [])
            if isinstance(tags, list):
                for t in tags:
                    if t.startswith("task_type:"):
                        created_task_type = t[len("task_type:"):]
                        break
        if created_task_type and created_task_type.strip().lower() == current_task_type.strip().lower():
            item["confidence"] = min(1.0, item.get("confidence", 0.5) + _ENCODING_BONUS)
            item["_encoding_specificity_match"] = True
    except Exception as e:
        log.debug("Encoding specificity check failed: %s", e)
    return item


# ---------------------------------------------------------------------------
# Item 114: Goal-directed attention allocation (Attention)
# ---------------------------------------------------------------------------
_URGENCY_WORDS = frozenset({"urgent", "critical", "breaking", "emergency", "blocker", "hotfix", "outage"})
_URGENCY_SOURCES = frozenset({"anti_patterns", "fails", "failures"})
_URGENCY_BOOST = 1.5


def _detect_urgency(query: str) -> bool:
    """Return True if the query contains urgency-signalling words."""
    try:
        low = query.lower()
        return any(w in low for w in _URGENCY_WORDS)
    except Exception:
        return False


def apply_urgency_boost(results: dict, query: str) -> None:
    """If query is urgent, boost anti_patterns and fails sources by 1.5x."""
    try:
        if not detect_urgency(query):
            return
        for source_name, hits in results.items():
            if not isinstance(hits, list):
                continue
            if source_name in _URGENCY_SOURCES:
                for h in hits:
                    h["confidence"] = min(1.0, h.get("confidence", 0.5) * _URGENCY_BOOST)
                    h["_urgency_boost"] = True
    except Exception as e:
        log.debug("Urgency boost failed: %s", e)


# ---------------------------------------------------------------------------
# Item 115: Semantic satiation — repeated concept penalty (Attention)
# ---------------------------------------------------------------------------
_SATIATION_THRESHOLD = 5
_SATIATION_PENALTY = 0.15
_STOP_WORDS = frozenset({
    "about", "after", "again", "against", "allow", "along", "always",
    "because", "before", "being", "between", "could", "during", "every",
    "found", "from", "have", "having", "here", "itself", "other",
    "should", "since", "their", "there", "these", "those", "through",
    "under", "until", "using", "where", "which", "while", "with",
    "would", "your",
})


def _extract_concepts(text: str) -> list:
    """Extract significant words (len >= 5, no stop words) as concepts."""
    try:
        words = re.findall(r"\b[a-z]{5,}\b", text.lower())
        return [w for w in words if w not in _STOP_WORDS]
    except Exception:
        return []


def apply_semantic_satiation(results: dict) -> None:
    """Penalise items dominated by over-represented concepts (5+ items)."""
    try:
        all_items = [h for hits in results.values() if isinstance(hits, list) for h in hits]
        if len(all_items) < _SATIATION_THRESHOLD:
            return

        concept_item_count: Counter = Counter()
        for h in all_items:
            text = (h.get("text") or h.get("title") or h.get("content") or "")
            concepts = set(_extract_concepts(text))
            for c in concepts:
                concept_item_count[c] += 1

        satiated = {c for c, n in concept_item_count.items() if n >= _SATIATION_THRESHOLD}
        if not satiated:
            return

        for h in all_items:
            text = (h.get("text") or h.get("title") or h.get("content") or "")
            concepts_in_item = set(_extract_concepts(text))
            overlap = concepts_in_item & satiated
            if len(overlap) >= 3:
                penalty = min(0.4, _SATIATION_PENALTY * len(overlap))
                h["confidence"] = max(0.05, h.get("confidence", 0.5) - penalty)
                h["_satiation_penalty"] = True
                h["_satiated_concepts"] = list(overlap)[:5]
    except Exception as e:
        log.debug("Semantic satiation failed: %s", e)


# ---------------------------------------------------------------------------
# Item 118: Contextual inhibition — suppress irrelevant domains (Search)
# ---------------------------------------------------------------------------
_DOMAIN_KEYWORDS: dict = {
    "security": {"security", "auth", "crypto", "token", "cert", "vuln", "exploit", "injection"},
    "testing": {"test", "mock", "pytest", "assert", "coverage", "fixture", "unit"},
    "deployment": {"deploy", "docker", "k8s", "kubernetes", "ci", "cd", "release", "prod"},
    "database": {"database", "sql", "sqlite", "query", "migration", "schema", "index"},
    "api": {"api", "rest", "endpoint", "http", "request", "response", "route"},
    "brain": {"brain", "memory", "knowledge", "consolidation", "salience", "confidence"},
}
_INHIBITION_PENALTY = 0.20


def _infer_query_domain(query: str) -> Optional[str]:
    """Infer the primary domain of a query. Returns domain string or None if ambiguous."""
    try:
        q_words = set(re.findall(r"\b[a-z]{3,}\b", query.lower()))
        best_domain = None
        best_count = 0
        for domain, keywords in _DOMAIN_KEYWORDS.items():
            overlap = len(q_words & keywords)
            if overlap > best_count:
                best_count = overlap
                best_domain = domain
        if best_count >= 2:
            return best_domain
        return None
    except Exception:
        return None


def apply_contextual_inhibition(results: dict, query: str) -> None:
    """Penalise results from unrelated domains by 20% when query domain is clear."""
    try:
        query_domain = infer_query_domain(query)
        if not query_domain:
            return
        for hits in results.values():
            if not isinstance(hits, list):
                continue
            for h in hits:
                item_domain = (h.get("domain") or "").lower()
                if item_domain and item_domain != query_domain:
                    h["confidence"] = max(0.05, h.get("confidence", 0.5) * (1.0 - _INHIBITION_PENALTY))
                    h["_contextual_inhibition"] = True
    except Exception as e:
        log.debug("Contextual inhibition failed: %s", e)


# ---------------------------------------------------------------------------
# Item 119: Memory strength vs accessibility distinction (Memory)
# ---------------------------------------------------------------------------
_HIGH_STRENGTH_THRESHOLD = 5
_LOW_ACCESS_THRESHOLD = 2


def apply_latent_knowledge_flag(item: dict) -> dict:
    """Flag items with high strength (evidence_count) but low accessibility (access_count)."""
    try:
        ec = int(item.get("evidence_count", 1))
        ac = int(item.get("access_count", 0))
        if ec >= _HIGH_STRENGTH_THRESHOLD and ac <= _LOW_ACCESS_THRESHOLD:
            item["_latent_knowledge"] = True
            item["confidence"] = min(1.0, item.get("confidence", 0.5) + 0.05)
    except Exception as e:
        log.debug("Latent knowledge flag failed: %s", e)
    return item


# ---------------------------------------------------------------------------
# Item 124: Surprise-based attention capture (P29)
# ---------------------------------------------------------------------------
def apply_surprise_attention(results: Dict[str, List[Any]]) -> None:
    """Bonus for items deviating > 2 std devs from domain average confidence."""
    try:
        domain_confidences: Dict[str, List[float]] = {}
        for hits in results.values():
            if not isinstance(hits, list):
                continue
            for h in hits:
                dom = h.get("domain", "")
                if dom:
                    domain_confidences.setdefault(dom, []).append(float(h.get("confidence", 0.5)))

        domain_stats: Dict[str, tuple] = {}
        for dom, confs in domain_confidences.items():
            if len(confs) >= 2:
                try:
                    mu = statistics.mean(confs)
                    sigma = statistics.stdev(confs)
                    domain_stats[dom] = (mu, sigma)
                except Exception:
                    pass

        for hits in results.values():
            if not isinstance(hits, list):
                continue
            for h in hits:
                dom = h.get("domain", "")
                if dom not in domain_stats:
                    continue
                mu, sigma = domain_stats[dom]
                if sigma < 0.01:
                    continue
                conf = float(h.get("confidence", 0.5))
                z_score = (conf - mu) / sigma
                if abs(z_score) >= 2.0:
                    bonus = round(min(0.15, abs(z_score - 2.0) * 0.03), 4)
                    h["confidence"] = min(1.0, conf + bonus)
                    h["_surprise_bonus"] = bonus
    except Exception as e:
        log.debug("apply_surprise_attention failed: %s", e)


# ---------------------------------------------------------------------------
# Item 126: Cortical column organization (P30)
# ---------------------------------------------------------------------------
def _organize_columns(results: Dict[str, List[Any]]) -> Dict[str, Any]:
    """Reorganize search results into cortical columns (domain groupings)."""
    try:
        all_hits: List[Any] = []
        for hits in results.values():
            if isinstance(hits, list):
                all_hits.extend(hits)

        columns: Dict[str, List[Any]] = {}
        for h in all_hits:
            dom = h.get("domain", "general") or "general"
            columns.setdefault(dom, []).append(h)

        organized: Dict[str, Any] = {}
        for dom, items in columns.items():
            try:
                items_sorted = sorted(
                    items,
                    key=lambda x: x.get("_wilson_lower", x.get("confidence", 0)),
                    reverse=True,
                )
                organized[dom] = {
                    "top": items_sorted[0],
                    "rest": items_sorted[1:],
                    "column_size": len(items_sorted),
                }
            except Exception:
                organized[dom] = {
                    "top": items[0] if items else {},
                    "rest": [],
                    "column_size": len(items),
                }

        return {"columns": organized, "flat": results}
    except Exception as e:
        log.debug("_organize_columns failed: %s", e)
        return {"columns": {}, "flat": results}

# Public aliases (backward compat)
apply_encoding_specificity = _apply_encoding_specificity
detect_urgency = _detect_urgency
infer_query_domain = _infer_query_domain
