#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Per-item scoring, filtering, and post-scoring enrichment for brain search.

Extracted from search_engine.py — all D/E/A/B dimensions, quality gate,
metamemory annotation, attention weighting, drift detection, spreading
activation, semantic priming, and deduplication live here.

P27/P28 extensions delegated to search_scoring_ext.py (items 113-119).
"""
import hashlib, logging
from pulse.core.brain_utils import text_similarity
from pulse.brain.provenance import get_provenance

try:
    from pulse.brain.brain_search.search_scoring_ext import (
        apply_encoding_specificity,   # Item 113
        apply_urgency_boost,          # Item 114
        apply_semantic_satiation,     # Item 115
        apply_contextual_inhibition,  # Item 118
        apply_latent_knowledge_flag,  # Item 119
    )
except Exception:
    apply_encoding_specificity = lambda item, tt: item
    apply_urgency_boost = lambda results, query: None
    apply_semantic_satiation = lambda results: None
    apply_contextual_inhibition = lambda results, query: None
    apply_latent_knowledge_flag = lambda item: item

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


def _load_contradiction_texts():
    """Return set of lowered contradiction texts (unresolved)."""
    try:
        from pulse.admin.audit.truth_verifier import get_registry
        out = set()
        for c in get_registry().get("contradictions", []):
            if not c.get("resolution"):
                for s in ("item_a", "item_b"):
                    out.add(c[s].get("text", "")[:80].lower())
        return out
    except Exception as e:
        log.debug("Contradiction registry load failed: %s", e)
        return set()


def _load_trust():
    """Return (get_trust_fn or None, trust_data dict)."""
    try:
        from pulse.admin.audit.agent_trust import get_trust, _load_trust as _lt
        return get_trust, _lt().get("agents", {})
    except Exception as e:
        log.debug("Agent trust load failed: %s", e)
        return None, {}


def _score_item(h, source_name, query, task_type, ctr_texts,
                get_trust_fn, trust_data, source_query_counts,
                tc_fn, ts_fn, wilson_fn):
    """Score and enrich a single search result item. Returns item or None."""
    # N41: Snapshot original confidence before any scoring modifiers touch it.
    # _retrieval_score tracks the post-scoring combined value separately.
    # _penalty_stack_depth counts how many modifiers fire.
    h["_original_confidence"] = h.get("confidence", 0.5)
    _penalty_depth = 0

    validity = h.get("validity", "valid")
    if validity in ("expired", "historical"):
        return None
    # Item 63: Superseded items stay searchable but marked
    if validity == "superseded":
        h["title"] = "[SUPERSEDED] " + h.get("title", "")
        h["confidence"] = h.get("confidence", 0.5) * 0.5
        _penalty_depth += 1
        try:
            from pulse.brain.reconsolidation import _resolve_superseded_chain
            resolved_id = _resolve_superseded_chain(h.get("id", ""), source_name)
            if resolved_id != h.get("id"):
                h["_resolved_to"] = resolved_id
        except Exception: pass
    # Item 186: Broadcast flag — shared critical knowledge gets confidence boost
    try:
        if h.get("broadcast") or h.get("_social_broadcast"):
            h["confidence"] = min(1.0, h.get("confidence", 0.5) + 0.15)
            h["_broadcast_boosted"] = True
            _penalty_depth += 1
    except Exception: pass
    # H12 FIX: temporal decay lives only in compute_salience(); skip here to avoid double-decay.
    ts = h.get("timestamp", "")
    # Item 85: Calibration factor
    try:
        from pulse.brain.metamemory import _get_calibration_factor as _gcf85
        h["confidence"] = min(1.0, h.get("confidence", 0.5) * _gcf85())
        _penalty_depth += 1
    except Exception: pass
    # Context match boost
    if task_type and h.get("type", "") == task_type:
        h["confidence"] = min(1.0, h.get("confidence", 0.5) * 1.1)
        h["_context_match"] = True
        _penalty_depth += 1
    # Item 113: Encoding specificity match
    h = apply_encoding_specificity(h, task_type)
    # Item 61: Context-dependent forgetting reinstatement
    try:
        _dom = h.get("domain", "")
        if (h.get("confidence", 0.5) < 0.2 and task_type
                and (h.get("type", "") == task_type
                     or (_dom and len(_dom) >= 3
                         and (" " + _dom + " ") in (" " + query.lower() + " ")))):
            h["confidence"] += 0.2
            h["_reinstated"] = True
            _penalty_depth += 1
    except Exception: pass
    # Temporal salience
    last_acc = h.get("last_accessed") or h.get("timestamp", "")
    if last_acc:
        try: h["salience"] = ts_fn(float(h.get("salience", 1.0)), last_acc)
        except Exception: pass
    ec = h.get("evidence_count", 1)
    # Item 77: Novelty bonus
    try:
        if int(h.get("access_count", 0)) == 0:
            h["confidence"] = min(1.0, h.get("confidence", 0.5) + 0.1)
            h["_novelty_bonus"] = True
            _penalty_depth += 1
    except Exception: pass
    # Wilson intervals
    lo, hi = wilson_fn(h.get("confidence", 0.5), ec)
    h["confidence_interval"] = [lo, hi]
    h["_wilson_lower"] = lo
    # Recency tiebreaker
    try:
        from pulse.core.temporal import _days_since
        d = min(_days_since(h.get("timestamp", "")) / 30, 1.0)
        h["_wilson_lower"] = lo + max(0, 0.05 * (1.0 - d))
    except Exception: pass
    # Item 82: Reward-gated ranking (FIX 5: MD5 hash key)
    try:
        _acc82 = int(h.get("access_count", 0))
        _txt82 = (h.get("content", "") or h.get("text", "") or h.get("title", ""))[:80].lower().strip()
        _key82 = hashlib.md5(_txt82.encode("utf-8")).hexdigest()[:12] if _txt82 else ""
        if _key82:
            from pulse.core.brain_db import get_conn as _gc82
            _cr82 = _gc82().execute(
                "SELECT retrieval_count FROM retrieval_metrics WHERE item_key=?", (_key82,)
            ).fetchone()
            if _cr82 and _acc82 > 0:
                h["confidence"] = min(1.0, h["confidence"] + 0.05 * min(_acc82, 5))
                h["_reward_boosted"] = True
                _penalty_depth += 1
            elif _cr82 and int(_cr82[0] or 0) > 5 and _acc82 == 0:
                h["confidence"] *= 0.85
                h["_reward_penalized"] = True
                _penalty_depth += 1
    except Exception: pass
    # Contradiction flagging
    hit_text = (h.get("content", "") or h.get("text", "") or h.get("title", ""))[:80].lower()
    if ctr_texts and hit_text and any(text_similarity(hit_text, ct) >= 0.5 for ct in ctr_texts):
        h["_contradiction_flag"] = True
        h["confidence"] = h.get("confidence", 0.5) * 0.7
        _penalty_depth += 1
    # Attention fatigue
    if source_query_counts.get(source_name, 0) > 50:
        h["confidence"] = h.get("confidence", 0.5) * 0.8
        h["_attention_fatigue"] = True
        _penalty_depth += 1
    # Agent trust (Dimension B)
    try:
        if get_trust_fn is not None and trust_data:
            sa = h.get("agent", "")
            if sa and sa.lower() in trust_data:
                ts2 = max(0.1, get_trust_fn(sa))
                h["confidence"] = h.get("confidence", 0.5) * ts2
                h["_source_trust"] = ts2
                lo, hi = wilson_fn(h["confidence"], ec)
                h["confidence_interval"] = [lo, hi]
                h["_wilson_lower"] = lo
                _penalty_depth += 1
    except Exception as e: log.debug("Trust lookup failed: %s", e)
    # Item 91: Confidence momentum (P23)
    try:
        _ec91 = int(h.get("evidence_count", 1))
        _ac91 = int(h.get("access_count", 0))
        if _ec91 > 3 and _ac91 > 0 and _ec91 / max(_ac91, 1) > 1.5:
            h["confidence"] = min(1.0, h["confidence"] * 1.1)
            h["_momentum_up"] = True
            _penalty_depth += 1
    except Exception: pass
    # FIX 8: IOR penalty
    try:
        from pulse.brain.working_memory import _get_ior_penalty
        ior = _get_ior_penalty(h.get("id", ""))
        if ior > 0:
            h["confidence"] *= (1.0 - ior)
            h["_ior_penalized"] = True
            _penalty_depth += 1
    except Exception: pass
    # N53: Penalty stacking cap — floor at 0.15 to prevent compound inhibition from zeroing legitimate results
    h["confidence"] = max(h.get("confidence", 0), 0.15)
    # N41: Stamp retrieval score (post-scoring confidence) and modifier count.
    # confidence field is kept at its original stored value via _original_confidence.
    # _retrieval_score holds the fully-modified value used for ranking.
    h["_retrieval_score"] = h.get("confidence", 0.5)
    h["_penalty_stack_depth"] = _penalty_depth
    # Item 119: Memory strength vs accessibility distinction
    h = apply_latent_knowledge_flag(h)
    # Provenance
    prov = get_provenance(h.get("fingerprint", ""))
    if prov:
        h["_provenance"] = prov
    # P46.5: Post-retrieval noise filter — reject short/conversational fragments
    _text_len = len(h.get("text", "") or h.get("content", "") or "")
    _src_type = h.get("source_type", "")
    if _text_len < 20 and h.get("salience", 0) < 2.0:
        return None  # Fragment too short with low salience
    if _src_type == "CONVERSATIONAL" and h.get("consolidation_count", 1) <= 1 and h.get("confidence", 0.5) < 0.5:
        return None  # Unconsolidated conversational noise
    # Item 93: Adaptive quality gate (P23)
    try:
        _cal = __import__("pulse.brain.metamemory", fromlist=["_get_calibration_factor"])
        _qg93 = min(0.6, max(0.2, 0.4 / max(_cal._get_calibration_factor(), 0.5)))
    except Exception:
        _qg93 = 0.4
    if h.get("confidence", 0.5) >= _qg93:
        try:
            if not h.get("_skip_epistemic"):
                from pulse.brain.epistemic import apply_epistemic_qualifiers
                h = apply_epistemic_qualifiers(h)
        except Exception as e: log.debug("Epistemic qualifier failed: %s", e)
        return h
    return None  # Below quality gate

def _fetch_superseded_replacement(item: dict) -> dict | None:
    """Item 190: Fetch the replacement item for a superseded entry (fail-open)."""
    try:
        import json as _json
        meta = _json.loads(item.get("metadata") or "{}")
        new_id = meta.get("superseded_by", "")
        if not new_id:
            return None
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        for table in ("lessons", "failures", "patterns", "facts", "decisions"):
            row = conn.execute(
                f"SELECT id, content, title, domain, confidence, type, validity, timestamp FROM {table} WHERE id=?",
                (new_id,)
            ).fetchone()
            if row:
                replacement = dict(row)
                replacement["_is_replacement_for"] = item.get("id", "")
                replacement["_replacement_source"] = table
                return replacement
    except Exception as e:
        log.debug("_fetch_superseded_replacement failed: %s", e)
    return None


def _apply_post_scoring(results, query, task_type):
    """Delegate to search_post_scoring (extracted for SoC)."""
    try:
        from pulse.brain.brain_search.search_post_scoring import apply_post_scoring_impl
        apply_post_scoring_impl(results, query, task_type,
                                apply_urgency_boost, apply_semantic_satiation,
                                apply_contextual_inhibition, log)
    except Exception as e:
        log.debug("_apply_post_scoring delegation failed: %s", e)
def score_and_filter_results(results, query, task_type, config, source_query_counts, agent_name=None):
    """Score, filter, and enrich all search results in-place. Returns results dict."""
    from pulse.core.temporal import temporal_confidence, temporal_salience
    from pulse.core.uncertainty import wilson_interval
    ctr_texts = _load_contradiction_texts()
    get_trust_fn, trust_data = _load_trust()
    try:  # Working memory: track session items for agent
        from pulse.brain.working_memory import _add_to_session
        _all_hits = [h for src in results.values() if isinstance(src, list) for h in src]
        if agent_name and _all_hits:
            _add_to_session(agent_name, _all_hits[:10])
    except Exception:
        pass

    for source_name in list(results.keys()):
        hits = results[source_name]
        if not isinstance(hits, list):
            continue
        enriched = []
        for h in hits:
            scored = _score_item(
                h, source_name, query, task_type, ctr_texts,
                get_trust_fn, trust_data, source_query_counts,
                temporal_confidence, temporal_salience, wilson_interval,
            )
            if scored is not None:
                enriched.append(scored)
                # Item 190: Surface the replacement alongside a superseded item
                try:
                    if scored.get("validity") == "superseded" or "[SUPERSEDED]" in scored.get("title", ""):
                        replacement = _fetch_superseded_replacement(scored)
                        if replacement is not None:
                            replacement["_superseded_chain"] = True
                            enriched.append(replacement)
                except Exception:
                    pass
        enriched.sort(key=lambda x: x.get("_wilson_lower", 0), reverse=True)
        # Item 108: Retrieval interference scoring — same-domain competitor suppresses lower-ranked items
        try:
            domain_seen = {}
            for h in enriched:
                dom = h.get("domain", "")
                if dom in domain_seen:
                    h["confidence"] *= 0.95  # Suppressed by same-domain competitor
                    h["_retrieval_interference"] = True
                else:
                    domain_seen[dom] = True
        except Exception:
            pass
        # Item 89: Confidence floor - never wipe a source entirely
        if not enriched and hits and isinstance(hits, list):
            best = max(hits, key=lambda x: x.get("confidence", 0))
            best["_below_gate"] = True
            enriched = [best]
        results[source_name] = enriched

    _apply_post_scoring(results, query, task_type)
    return results
