#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Core per-item scoring engine for brain search.

Extracted from search_scoring.py for Law 7 SoC (files <= 300 lines).
Contains _score_item: the 184-line D/E/A/B dimension scoring function.
"""
import hashlib
import logging

from pulse.core.brain_utils import text_similarity

try:
    from pulse.brain.brain_search.search_scoring_ext import (
        apply_encoding_specificity,
        apply_latent_knowledge_flag,
    )
except Exception:
    apply_encoding_specificity = lambda item, tt: item
    apply_latent_knowledge_flag = lambda item: item

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


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.
    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.plasticity.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
    # Phase B Item 10: Bidirectional Affective Retrieval
    if source_name == "failures":
        try:
            import pulse.core.interoception as interoception
            interoception.load_signals()
            err_rate = interoception._signals.get("error_rate", {}).get("current_value", 0.0)
            load = interoception._signals.get("agent_load", {}).get("current_value", 0.0)
            if err_rate > 0.15 or load > 5.0:
                h["salience"] = float(h.get("salience", 1.0)) * 5.0
                h["_affective_salience"] = 5.0
        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.metacognition.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)
    # Temporal decay: multiply confidence by decay_score (Audit 5 P0)
    _ds = float(h.get("decay_score", 1.0))
    if _ds < 1.0:
        h["confidence"] = h.get("confidence", 0.5) * max(0.3, _ds)
        h["_decay_applied"] = True
    # Item 77: Exploration bonus (age-weighted for zero-access items)
    try:
        if int(h.get("access_count", 0)) == 0:
            import math
            _ts77 = h.get("timestamp", "")
            _age_days = 1.0
            if _ts77:
                try:
                    from pulse.core.temporal import _days_since
                    _age_days = max(1.0, _days_since(_ts77))
                except Exception:
                    pass
            # Older unseen items get stronger boost (log curve, 0.08-0.20)
            _explore_boost = min(0.20, 0.08 + 0.04 * math.log1p(_age_days / 7.0))
            h["confidence"] = min(1.0, h.get("confidence", 0.5) + _explore_boost)
            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 (P47: boosted from +0.05/30d to +0.15/7d)
    try:
        from pulse.core.temporal import _days_since
        _days = _days_since(h.get("timestamp", ""))
        d = min(_days / 7.0, 1.0)  # P47: 7-day window (was 30)
        h["_wilson_lower"] = lo + max(0, 0.15 * (1.0 - d))  # P47: +0.15 max (was 0.05)
    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.memory.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 zeroing results
    h["confidence"] = max(h.get("confidence", 0), 0.15)
    # N41: Stamp retrieval score (post-scoring confidence) and modifier count.
    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
    try:
        from pulse.brain.integrity.provenance import get_provenance
        prov = get_provenance(h.get("fingerprint", ""))
        if prov:
            h["_provenance"] = prov
    except Exception:
        pass
    # P46.5: Post-scoring 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
    # P47: 48h immunity — fresh items skip the noise filter
    _is_recent = False
    try:
        from pulse.core.temporal import _days_since
        _is_recent = _days_since(h.get("timestamp", "")) < 2.0
    except Exception:
        pass
    if (not _is_recent and _src_type == "CONVERSATIONAL"
            and h.get("consolidation_count", 1) <= 1
            and h.get("confidence", 0.5) < 0.5):
        return None  # Unconsolidated conversational noise (not recent)
    # Item 93: Adaptive quality gate (P23)
    try:
        _cal = __import__("pulse.brain.metacognition.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.workspace.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
