"""Layer 1.5 cross-signal dissonance detection."""

from __future__ import annotations

from dataclasses import dataclass
import re

import numpy as np

from cognitive_signal.models.types import SignalVector

_WORD_PATTERN = re.compile(r"[A-Za-z']+")
_CLAUSE_PATTERN = re.compile(r"[.!?;]+")

CERTAINTY_TERMS = {
    "definitely",
    "certainly",
    "always",
    "never",
    "completely",
    "totally",
    "absolutely",
    "fine",
    "okay",
    "sure",
}

COMPLEXITY_MARKERS = {
    "because",
    "although",
    "however",
    "might",
    "maybe",
    "perhaps",
    "if",
    "while",
    "but",
    "though",
    "unless",
}

NEGATION_TERMS = {
    "no",
    "not",
    "never",
    "nobody",
    "none",
    "don't",
    "doesn't",
    "didn't",
    "can't",
    "cannot",
    "won't",
}

SOCIAL_TERMS = {
    "anyone",
    "anybody",
    "everyone",
    "somebody",
    "someone",
    "people",
    "friends",
    "family",
    "he",
    "she",
    "they",
    "them",
    "we",
    "us",
    "others",
}

DEFAULT_WEIGHTS = {
    "protest_positivity": 0.30,
    "passive_agency_talk": 0.25,
    "social_isolation_conflict": 0.25,
    "forced_calm": 0.20,
}


def _clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
    return max(lo, min(hi, value))


def _tokenize_words(text: str) -> list[str]:
    return [token.lower() for token in _WORD_PATTERN.findall(text)]


def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    a_vec = np.asarray(a, dtype=np.float32).reshape(-1)
    b_vec = np.asarray(b, dtype=np.float32).reshape(-1)
    if a_vec.size == 0 or b_vec.size == 0 or a_vec.shape != b_vec.shape:
        return 0.0
    a_norm = float(np.linalg.norm(a_vec))
    b_norm = float(np.linalg.norm(b_vec))
    if a_norm == 0.0 or b_norm == 0.0:
        return 0.0
    return float(np.dot(a_vec, b_vec) / (a_norm * b_norm))


@dataclass(slots=True)
class DissonanceVector:
    protest_positivity: float
    passive_agency_talk: float
    social_isolation_conflict: float
    forced_calm: float
    overall_dissonance: float


class DissonanceDetector:
    def __init__(self, weights: dict[str, float] | None = None):
        merged = DEFAULT_WEIGHTS.copy()
        if weights:
            for key, value in weights.items():
                if key in merged:
                    merged[key] = float(value)
        total = sum(merged.values())
        if total <= 0.0:
            raise ValueError("Dissonance weights must sum to a positive value.")
        self.weights = {key: value / total for key, value in merged.items()}

    def detect(
        self,
        signal_vector: SignalVector,
        recent_signals: list[SignalVector] | None = None,
        text: str | None = None,
    ) -> DissonanceVector:
        history = recent_signals or []
        protest_positivity = self._score_protest_positivity(signal_vector, history, text)
        passive_agency_talk = self._score_passive_agency_talk(signal_vector)
        social_isolation_conflict = self._score_social_isolation_conflict(signal_vector, text)
        forced_calm = self._score_forced_calm(signal_vector, history)
        overall_dissonance = _clamp(
            protest_positivity * self.weights["protest_positivity"]
            + passive_agency_talk * self.weights["passive_agency_talk"]
            + social_isolation_conflict * self.weights["social_isolation_conflict"]
            + forced_calm * self.weights["forced_calm"]
        )
        return DissonanceVector(
            protest_positivity=protest_positivity,
            passive_agency_talk=passive_agency_talk,
            social_isolation_conflict=social_isolation_conflict,
            forced_calm=forced_calm,
            overall_dissonance=overall_dissonance,
        )

    def _score_protest_positivity(
        self,
        signal_vector: SignalVector,
        recent_signals: list[SignalVector],
        text: str | None,
    ) -> float:
        valence_component = _clamp((signal_vector.valence - 0.5) / 0.5)
        certainty_component = self._certainty_component(text)
        low_complexity_component = self._low_complexity_component(text)
        repetition_component = self._repetition_component(
            signal_vector.embedding, recent_signals
        )
        score = (
            0.35 * valence_component
            + 0.25 * certainty_component
            + 0.20 * low_complexity_component
            + 0.20 * repetition_component
        )
        return _clamp(score)

    def _score_passive_agency_talk(self, signal_vector: SignalVector) -> float:
        agency_component = _clamp((signal_vector.agency - 0.3) / 0.7)
        inaction_component = _clamp((0.6 - signal_vector.action_ratio) / 0.6)
        return _clamp((0.5 * agency_component) + (0.5 * inaction_component))

    def _score_social_isolation_conflict(
        self, signal_vector: SignalVector, text: str | None
    ) -> float:
        low_social_component = _clamp((0.4 - signal_vector.social_orientation) / 0.4)
        negated_social_component = self._negated_social_component(text)
        return _clamp((0.4 * low_social_component) + (0.6 * negated_social_component))

    def _score_forced_calm(
        self, signal_vector: SignalVector, recent_signals: list[SignalVector]
    ) -> float:
        if len(recent_signals) < 3:
            return 0.0
        low_arousal_component = _clamp((0.4 - signal_vector.arousal) / 0.4)
        self_monitoring_component = _clamp((signal_vector.self_focus - 0.6) / 0.4)
        low_volatility_component = self._low_volatility_component(signal_vector, recent_signals)
        return _clamp(
            (
                low_arousal_component
                + self_monitoring_component
                + low_volatility_component
            )
            / 3.0
        )

    def _certainty_component(self, text: str | None) -> float:
        if not text:
            return 0.0
        words = _tokenize_words(text)
        if not words:
            return 0.0
        certainty_hits = sum(word in CERTAINTY_TERMS for word in words)
        return _clamp((certainty_hits / len(words)) * 4.0)

    def _low_complexity_component(self, text: str | None) -> float:
        if not text:
            return 0.0
        words = _tokenize_words(text)
        if not words:
            return 0.0
        lexical_diversity = len(set(words)) / len(words)
        marker_hits = sum(word in COMPLEXITY_MARKERS for word in words)
        marker_density_normalized = _clamp((marker_hits / len(words)) * 4.0)
        complexity = (0.6 * lexical_diversity) + (0.4 * marker_density_normalized)
        return _clamp(1.0 - _clamp(complexity))

    def _repetition_component(
        self, current_embedding: np.ndarray, recent_signals: list[SignalVector]
    ) -> float:
        if not recent_signals:
            return 0.0
        similarities = [
            _cosine_similarity(current_embedding, past.embedding) for past in recent_signals
        ]
        if not similarities:
            return 0.0
        top_three_mean = float(np.mean(sorted(similarities, reverse=True)[:3]))
        return _clamp((top_three_mean - 0.70) / 0.30)

    def _negated_social_component(self, text: str | None) -> float:
        if not text:
            return 0.0
        clauses = [segment.strip() for segment in _CLAUSE_PATTERN.split(text.lower()) if segment.strip()]
        if not clauses:
            return 0.0
        flagged_clauses = 0
        for clause in clauses:
            words = set(_tokenize_words(clause))
            if not words:
                continue
            has_negation = any(word in NEGATION_TERMS for word in words)
            has_social_ref = any(word in SOCIAL_TERMS for word in words)
            if has_negation and has_social_ref:
                flagged_clauses += 1
        return _clamp(flagged_clauses / len(clauses))

    def _low_volatility_component(
        self, current_signal: SignalVector, recent_signals: list[SignalVector]
    ) -> float:
        series = recent_signals[-10:] + [current_signal]
        arousal_series = np.asarray([signal.arousal for signal in series], dtype=np.float32)
        valence_series = np.asarray([signal.valence for signal in series], dtype=np.float32)
        volatility = (0.5 * float(np.std(arousal_series))) + (
            0.5 * float(np.std(valence_series))
        )
        return _clamp((0.12 - volatility) / 0.12)


__all__ = ["DissonanceDetector", "DissonanceVector"]
