#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Search scoring P36 — Item 154: Memory interference resolution.

Kept separate from search_scoring_ext.py / search_scoring_p34.py to respect
the 300-line modular ceiling.

  - Item 154: Memory interference resolution (suppress contradicting lower-conf item)
"""
import logging
import re
from typing import Dict, List, Any

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

# ---------------------------------------------------------------------------
# Item 154: Memory interference resolution (Memory)
# ---------------------------------------------------------------------------
_ALWAYS_RE = re.compile(r'\balways\b', re.IGNORECASE)
_NEVER_RE  = re.compile(r'\bnever\b',  re.IGNORECASE)


def _is_always_claim(text: str) -> bool:
    return bool(_ALWAYS_RE.search(text))


def _is_never_claim(text: str) -> bool:
    return bool(_NEVER_RE.search(text))


def _extract_subject(text: str) -> str:
    """Extract a simple subject word from an always/never claim for matching."""
    try:
        # Remove the always/never word and extract first meaningful word
        cleaned = _ALWAYS_RE.sub("", _NEVER_RE.sub("", text)).lower()
        words = re.findall(r"\b[a-z]{4,}\b", cleaned)
        return words[0] if words else ""
    except Exception:
        return ""


def resolve_memory_interference(results: Dict[str, List[Any]]) -> int:
    """Suppress contradicting lower-confidence items in search results.

    When two results directly contradict each other (one says "always X",
    other says "never X"), keep the higher-confidence one and mark the lower
    as suppressed (_interference_suppressed=True, confidence set to 0.01).

    Returns: count of suppressed items.
    Fails open (returns 0) on any error.
    """
    suppressed = 0
    try:
        all_items: List[Any] = [
            h for hits in results.values()
            if isinstance(hits, list) for h in hits
        ]
        if len(all_items) < 2:
            return 0

        # Collect always-claims and never-claims by subject
        always_claims: Dict[str, List[Any]] = {}
        never_claims: Dict[str, List[Any]] = {}

        for item in all_items:
            text = (item.get("text") or item.get("title") or item.get("content") or "")
            if _is_always_claim(text):
                subj = _extract_subject(text)
                if subj:
                    always_claims.setdefault(subj, []).append(item)
            elif _is_never_claim(text):
                subj = _extract_subject(text)
                if subj:
                    never_claims.setdefault(subj, []).append(item)

        # Find subject overlaps — direct contradictions
        overlap_subjects = set(always_claims.keys()) & set(never_claims.keys())
        for subj in overlap_subjects:
            candidates = always_claims[subj] + never_claims[subj]
            if len(candidates) < 2:
                continue
            # Keep highest confidence; suppress the rest
            best = max(candidates, key=lambda x: float(x.get("confidence", 0.0)))
            for item in candidates:
                if item is not best:
                    item["_interference_suppressed"] = True
                    item["_interference_winner"] = id(best)
                    item["confidence"] = 0.01
                    suppressed += 1

    except Exception as exc:
        log.debug("resolve_memory_interference failed: %s", exc)
    return suppressed


# ---------------------------------------------------------------------------
# Item 157: Retrieval fluency signal (Search)
# ---------------------------------------------------------------------------
import time as _time
from collections import deque

_LATENCY_WINDOW: deque = deque(maxlen=20)   # last 20 search latencies (seconds)
_LATENCY_WARN_RATIO = 1.5   # warn if recent avg > 1.5x baseline avg
_MIN_SAMPLES_FOR_WARN = 5   # need at least 5 samples before warning


def record_search_latency(seconds: float) -> None:
    """Record a search latency measurement. Fails open."""
    try:
        _LATENCY_WINDOW.append(float(seconds))
    except Exception:
        pass


def get_search_latency_status() -> Dict[str, Any]:
    """Return latency trend and warning if search is getting slower.

    Returns:
        {sample_count, avg_latency, recent_avg, warning}
    Fails open (returns zero latency, no warning) on any error.
    """
    result: Dict[str, Any] = {
        "sample_count": 0,
        "avg_latency": 0.0,
        "recent_avg": 0.0,
        "warning": None,
    }
    try:
        samples = list(_LATENCY_WINDOW)
        n = len(samples)
        if n == 0:
            return result
        avg = sum(samples) / n
        # Recent = last quarter of window vs full average
        recent_n = max(1, n // 4)
        recent_avg = sum(samples[-recent_n:]) / recent_n
        warning = None
        if n >= _MIN_SAMPLES_FOR_WARN and avg > 0 and recent_avg > avg * _LATENCY_WARN_RATIO:
            warning = (
                f"!! SEARCH LATENCY INCREASING: recent avg {recent_avg:.3f}s vs "
                f"baseline {avg:.3f}s — index may need rebuild or DB vacuum"
            )
        result.update({
            "sample_count": n,
            "avg_latency": round(avg, 4),
            "recent_avg": round(recent_avg, 4),
            "warning": warning,
        })
    except Exception as exc:
        log.debug("get_search_latency_status failed: %s", exc)
    return result


# ---------------------------------------------------------------------------
# Item 163: Semantic neighborhood density bonus (Search)
# ---------------------------------------------------------------------------
_DENSITY_BONUS = 0.08   # bonus for well-connected items
_DENSITY_THRESHOLD = 3  # items need >=3 graph neighbours to get bonus


def apply_neighborhood_density_bonus(results: Dict[str, List[Any]]) -> None:
    """Give a slight confidence bonus to items in dense graph neighbourhoods.

    Well-connected knowledge (many graph edges) is treated as more reliable.
    Fails open silently.
    """
    try:
        from pulse.core.brain_db import get_conn
        conn = get_conn()

        all_items: List[Any] = [
            h for hits in results.values()
            if isinstance(hits, list) for h in hits
        ]
        for item in all_items:
            nid = str(item.get("node_id") or item.get("id") or "")
            if not nid:
                continue
            try:
                row = conn.execute(
                    "SELECT COUNT(*) FROM graph_edges "
                    "WHERE from_node = ? OR to_node = ?",
                    (nid, nid)
                ).fetchone()
                degree = int(row[0] or 0)
                if degree >= _DENSITY_THRESHOLD:
                    item["confidence"] = min(
                        1.0, item.get("confidence", 0.5) + _DENSITY_BONUS
                    )
                    item["_density_bonus"] = True
                    item["_graph_degree"] = degree
            except Exception:
                pass  # per-item fail open
    except Exception as exc:
        log.debug("apply_neighborhood_density_bonus failed: %s", exc)


# ---------------------------------------------------------------------------
# Item 168: Search diversity metric (Search)
# ---------------------------------------------------------------------------

def compute_search_diversity(results: Dict[str, List[Any]]) -> Dict[str, Any]:
    """Compute how many unique domains are represented in search results.

    Low diversity = potential tunnel vision. Returns diversity report.
    Fails open (returns empty result) on any error.
    """
    result: Dict[str, Any] = {
        "total_items": 0,
        "unique_domains": 0,
        "diversity_score": 0.0,
        "domains": [],
        "warning": None,
    }
    try:
        all_items: List[Any] = [
            h for hits in results.values()
            if isinstance(hits, list) for h in hits
        ]
        n = len(all_items)
        if n == 0:
            return result
        domains = set()
        for item in all_items:
            d = (item.get("domain") or "").strip()
            if d:
                domains.add(d)
        unique = len(domains)
        # Diversity score: unique_domains / sqrt(total_items) capped at 1.0
        import math
        diversity_score = round(min(1.0, unique / max(1, math.sqrt(n))), 3)
        warning = None
        if n >= 5 and unique <= 1:
            warning = (
                "!! LOW SEARCH DIVERSITY: All results from a single domain — "
                "potential tunnel vision. Consider broadening the query."
            )
        result.update({
            "total_items": n,
            "unique_domains": unique,
            "diversity_score": diversity_score,
            "domains": sorted(domains),
            "warning": warning,
        })
    except Exception as exc:
        log.debug("compute_search_diversity failed: %s", exc)
    return result
