"""
Phase 3A + Phase 4A: Goal-conditioned attention vectors.
Weights search sources based on task type or context priorities.
Phase 4: Blends static weights with empirically learned weights
from accumulated reward credit data (RPE prompt adaptation).
"""
import logging

log = logging.getLogger("pulse.attention")


def get_attention_weights(task_type: str = "general") -> dict:
    """
    Returns multipliers for 17 search sources based on task type.
    Phase 4: Blends static priors with learned weights when sufficient data exists.
    Baseline is 1.0. Blend: BLEND_RATIO * learned + (1 - BLEND_RATIO) * static.
    Cold start (no learned data): static weights returned unchanged.
    """
    weights = {
        "hot_cache": 1.0,
        "index": 1.0,
        "predictions": 1.0,
        "anti_patterns": 1.0,
        "graph": 1.0,
        "facts": 1.0,
        "episodic": 1.0,
        "working_memory": 1.0,
        "decisions": 1.0,
        "wins": 1.0,
        "fails": 1.0,
        "domains": 1.0,
        "patterns": 1.0,
        "evidence": 1.0,
        "private": 1.0,
        "schemas": 1.0,
        "procedures": 1.0
    }
    
    task_type = str(task_type).lower()

    # Derive canonical type — must match keys stored by update_from_credits
    if "debug" in task_type or "fix" in task_type:
        _canonical = "debug"
        weights["fails"] = 1.5
        weights["anti_patterns"] = 1.5
        weights["predictions"] = 1.5
        weights["episodic"] = 1.2

    elif "arch" in task_type or "design" in task_type:
        _canonical = "architecture"
        weights["schemas"] = 1.5
        weights["procedures"] = 1.5
        weights["graph"] = 1.5
        weights["patterns"] = 1.2

    elif "feature" in task_type or "impl" in task_type:
        _canonical = "feature"
        weights["patterns"] = 1.3
        weights["wins"] = 1.3
        # In reality DMN is not a standard search source, but we boost relevant concepts
        weights["procedures"] = 1.3

    elif "review" in task_type or "audit" in task_type:
        _canonical = "review"
        weights["anti_patterns"] = 1.5
        weights["decisions"] = 1.5
        weights["fails"] = 1.2

    else:
        _canonical = "general"

    # Phase 4: Blend with empirically learned weights (RPE prompt adaptation)
    try:
        from pulse.brain.metacognition.learned_prompt import get_learned_weights, BLEND_RATIO
        learned = get_learned_weights(_canonical)
        if learned:
            for source, learned_w in learned.items():
                if source in weights:
                    # Clamp to safe range in case of corrupted/edited weights file
                    learned_w = max(0.3, min(2.5, float(learned_w)))
                    # Blend: BLEND_RATIO * learned + (1 - BLEND_RATIO) * static
                    static_w = weights[source]
                    weights[source] = round(
                        BLEND_RATIO * learned_w + (1 - BLEND_RATIO) * static_w, 3)
    except Exception as e:
        # Cold start never throws (get_learned_weights returns None → if-guard).
        # Any exception here is an unexpected error; warn so it's visible in logs.
        log.warning("Learned weight blending failed (static fallback active): %s", e)

    return weights

def apply_attention_to_results(results: dict, task_type: str) -> dict:
    """Biased competition: boost attended sources, suppress competitors (Phase 3).

    Desimone & Duncan 1995: boosting one representation suppresses competitors.
    Sources with weight > 1.0 get boosted. Sources with weight 1.0 get
    suppressed proportionally to how much the boosted sources dominate.
    """
    if str(task_type).lower() == "general":
        return results  # No bias for general queries
    weights = get_attention_weights(task_type)

    # Find max boost factor — used to compute suppression
    boosted = {s: w for s, w in weights.items() if w > 1.0}
    if not boosted:
        return results
    # Suppression factor: non-boosted sources get inhibited
    suppression = 0.7  # 30% reduction for non-boosted sources

    for source, hits in results.get("results", {}).items():
        if not isinstance(hits, list):
            continue
        weight = weights.get(source, 1.0)
        for hit in hits:
            if weight > 1.0:
                # Boosted source: enhance
                if "salience" in hit:
                    hit["_attention_salience"] = float(hit["salience"]) * weight
                if "confidence" in hit:
                    hit["_attention_confidence"] = min(1.0, float(hit["confidence"]) * weight)
            else:
                # Non-boosted: lateral inhibition (suppress)
                if "salience" in hit:
                    hit["_attention_salience"] = float(hit["salience"]) * suppression
                if "confidence" in hit:
                    hit["_attention_confidence"] = float(hit["confidence"]) * suppression
                hit["_inhibited_by"] = list(boosted.keys())[:3]
    return results
class AttentionCompetition:
    """
    Phase B: Item-Level Competitive Suppression.
    Actively suppresses semantically redundant items in the workspace, 
    rather than just scaling source domains.
    """
    def __init__(self, similarity_threshold: float = 0.75):
        self.similarity_threshold = similarity_threshold
        
    def _jaccard_similarity(self, text1: str, text2: str) -> float:
        set1 = set(str(text1).lower().split())
        set2 = set(str(text2).lower().split())
        if not set1 or not set2:
            return 0.0
        return len(set1.intersection(set2)) / len(set1.union(set2))

    def suppress_redundant(self, items: list[dict]) -> list[dict]:
        """
        Takes a list of items (dicts).
        Sorts by activation metric (highest first).
        Iterates and suppresses items that are too similar to already accepted items.
        Returns the filtered list.
        """
        def get_score(item):
            return float(item.get("_gw_activation", item.get("salience", item.get("confidence", 1.0))))
            
        sorted_items = sorted(items, key=get_score, reverse=True)
        accepted = []
        
        for item in sorted_items:
            # Extract text representation
            text = str(item.get("description", item.get("content", item.get("want", item.get("text", item.get("choice", ""))))))
            if not text:
                accepted.append(item)
                continue
                
            is_redundant = False
            for acc_item in accepted:
                acc_text = str(acc_item.get("description", acc_item.get("content", acc_item.get("want", acc_item.get("text", acc_item.get("choice", ""))))))
                if self._jaccard_similarity(text, acc_text) > self.similarity_threshold:
                    is_redundant = True
                    # Re-entrant strengthening: the accepted item is reinforced by the redundant signal
                    if "_gw_activation" in acc_item:
                        acc_item["_gw_activation"] = min(1.0, float(acc_item["_gw_activation"]) * 1.1)
                    elif "salience" in acc_item:
                        acc_item["salience"] = min(10.0, float(acc_item["salience"]) * 1.1)
                    break
                    
            if not is_redundant:
                accepted.append(item)
                
        return accepted
