"""Layer 1 orthogonal signal extractors."""

from __future__ import annotations

import re
from typing import Any

import numpy as np

from cognitive_signal.models.types import JournalEntry, SignalVector

_WORD_PATTERN = re.compile(r"[A-Za-z']+")
_ALL_CAPS_PATTERN = re.compile(r"\b[A-Z]{3,}\b")
_FEEL_PATTERN = re.compile(
    r"\bi\s+(?:feel|felt|am feeling|was feeling)\s+([a-z']+)\b", re.IGNORECASE
)
_LOVE_HATE_PATTERN = re.compile(r"\bi\s+(?:love|hate)\b", re.IGNORECASE)

FIRST_PERSON_PRONOUNS = {"i", "me", "my", "mine", "myself"}
ALL_PRONOUNS = {
    "i",
    "me",
    "my",
    "mine",
    "myself",
    "you",
    "your",
    "yours",
    "yourself",
    "he",
    "him",
    "his",
    "himself",
    "she",
    "her",
    "hers",
    "herself",
    "it",
    "its",
    "itself",
    "we",
    "us",
    "our",
    "ours",
    "ourselves",
    "they",
    "them",
    "their",
    "theirs",
    "themselves",
}

THIRD_PERSON_PRONOUNS = {
    "he",
    "him",
    "his",
    "himself",
    "she",
    "her",
    "hers",
    "herself",
    "they",
    "them",
    "their",
    "theirs",
    "themselves",
}

HIGH_AROUSAL_SCORES = {
    "furious": 0.95,
    "ecstatic": 0.95,
    "terrified": 0.9,
    "thrilled": 0.9,
    "panicked": 0.9,
    "euphoric": 0.95,
    "enraged": 0.95,
    "hysterical": 0.9,
    "devastating": 0.85,
    "explosive": 0.85,
    "manic": 0.85,
    "frantic": 0.85,
    "exhilarated": 0.9,
    "horrified": 0.9,
    "seething": 0.9,
    "electrifying": 0.85,
    "agonizing": 0.85,
    "raging": 0.9,
    "screaming": 0.8,
    "desperate": 0.8,
}

MEDIUM_HIGH_AROUSAL_SCORES = {
    "amazing": 0.6,
    "wonderful": 0.58,
    "incredible": 0.62,
    "fantastic": 0.6,
    "excited": 0.65,
    "anxious": 0.6,
    "frustrated": 0.6,
    "angry": 0.6,
    "passionate": 0.6,
    "overwhelmed": 0.62,
    "stunned": 0.58,
    "shocked": 0.6,
    "amazed": 0.58,
    "disgusted": 0.62,
    "jealous": 0.58,
    "ashamed": 0.55,
    "proud": 0.55,
    "triumphant": 0.65,
    "heartbroken": 0.65,
    "devastated": 0.67,
    "enthusiastic": 0.62,
    "obsessed": 0.62,
    "determined": 0.58,
    "driven": 0.57,
    "terrible": 0.62,
    "awful": 0.62,
    "horrible": 0.65,
}

MEDIUM_AROUSAL_SCORES = {
    "happy": 0.4,
    "sad": 0.4,
    "worried": 0.42,
    "nervous": 0.45,
    "stressed": 0.46,
    "delighted": 0.43,
    "annoyed": 0.4,
    "confused": 0.38,
    "hopeful": 0.35,
    "grateful": 0.33,
    "guilty": 0.4,
    "lonely": 0.42,
    "motivated": 0.36,
    "inspired": 0.37,
    "curious": 0.34,
    "surprised": 0.45,
    "interested": 0.3,
    "interesting": 0.3,
    "uncomfortable": 0.43,
    "disappointed": 0.4,
    "nostalgic": 0.32,
    "sparking": 0.34,
    "spark": 0.34,
    "good": 0.3,
    "lost": 0.38,
    "excellent": 0.25,
    "wanting": 0.15,
    "finish": 0.16,
}

LOW_AROUSAL_SCORES = {
    "okay": 0.2,
    "fine": 0.2,
    "calm": 0.15,
    "content": 0.15,
    "tired": 0.22,
    "bored": 0.2,
    "relaxed": 0.12,
    "peaceful": 0.1,
    "gentle": 0.1,
    "quiet": 0.1,
    "mellow": 0.14,
    "satisfied": 0.16,
    "steady": 0.12,
    "comfortable": 0.14,
    "pleasant": 0.14,
    "easy": 0.12,
    "soft": 0.1,
    "settled": 0.12,
    "reflective": 0.18,
    "thoughtful": 0.18,
}

EMOTION_AROUSAL_SCORES = (
    HIGH_AROUSAL_SCORES
    | MEDIUM_HIGH_AROUSAL_SCORES
    | MEDIUM_AROUSAL_SCORES
    | LOW_AROUSAL_SCORES
)

INTENSITY_UP_MODIFIERS = {
    "very",
    "really",
    "extremely",
    "absolutely",
    "completely",
    "totally",
    "so",
}
INTENSITY_DOWN_MODIFIERS_SINGLE = {"slightly", "somewhat"}
INTENSITY_DOWN_MODIFIERS_PHRASES = {"a bit", "kind of", "sort of"}

ACTION_VERBS = {
    "run",
    "build",
    "write",
    "call",
    "send",
    "make",
    "move",
    "start",
    "finish",
    "ship",
    "lead",
    "meet",
    "drive",
    "fix",
    "push",
    "deliver",
}

COGNITIVE_STATIVE_VERBS = {
    "think",
    "feel",
    "wonder",
    "seem",
    "believe",
    "know",
    "consider",
    "realize",
    "imagine",
    "hope",
    "worry",
    "reflect",
    "guess",
}


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


def tokenize_words(text: str) -> list[str]:
    return _WORD_PATTERN.findall(text)


def _load_spacy_model(model_name: str):
    try:
        import spacy
    except ImportError as exc:
        raise ImportError(
            "spaCy is required for this extractor. Install with: pip install spacy"
        ) from exc

    try:
        return spacy.load(model_name)
    except OSError:
        try:
            from spacy.cli import download as spacy_download

            spacy_download(model_name)
        except Exception as exc:
            raise RuntimeError(
                f"Unable to auto-download spaCy model '{model_name}'. "
                "Install manually with: python -m spacy download "
                f"{model_name}"
            ) from exc
        return spacy.load(model_name)


class SemanticEmbedder:
    def __init__(self, model_name: str = "all-MiniLM-L6-v2", model: Any | None = None):
        self.model_name = model_name
        if model is not None:
            self.model = model
            return
        try:
            from sentence_transformers import SentenceTransformer
        except ImportError as exc:
            raise ImportError(
                "sentence-transformers is required. "
                "Install with: pip install sentence-transformers"
            ) from exc
        self.model = SentenceTransformer(model_name)

    def embed(self, text: str) -> np.ndarray:
        embedding = self.model.encode(text, convert_to_numpy=True)
        array = np.asarray(embedding, dtype=np.float32).reshape(-1)
        if array.shape != (384,):
            raise ValueError(
                f"Expected embedding shape (384,), got {array.shape} from model "
                f"'{self.model_name}'."
            )
        return array


class ValenceExtractor:
    def __init__(self, analyzer: Any | None = None):
        if analyzer is not None:
            self.analyzer = analyzer
            return
        try:
            from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
        except ImportError as exc:
            raise ImportError(
                "vaderSentiment is required. Install with: pip install vaderSentiment"
            ) from exc
        self.analyzer = SentimentIntensityAnalyzer()

    def extract(self, text: str) -> float:
        compound = float(self.analyzer.polarity_scores(text).get("compound", 0.0))
        return _clamp(compound, -1.0, 1.0)


class ArousalExtractor:
    def extract(self, text: str) -> float:
        words = [word.lower() for word in tokenize_words(text)]
        if not words:
            return 0.0

        matched_scores: list[float] = []
        for index, word in enumerate(words):
            base = EMOTION_AROUSAL_SCORES.get(word)
            if base is None:
                continue

            adjusted = base
            previous = words[index - 1] if index > 0 else ""
            previous_pair = " ".join(words[max(0, index - 2) : index]) if index > 0 else ""
            if previous in INTENSITY_UP_MODIFIERS:
                adjusted *= 1.3
            if (
                previous in INTENSITY_DOWN_MODIFIERS_SINGLE
                or previous_pair in INTENSITY_DOWN_MODIFIERS_PHRASES
            ):
                adjusted *= 0.7
            matched_scores.append(min(1.0, adjusted))

        lowered_text = text.lower()
        for match in _FEEL_PATTERN.finditer(lowered_text):
            emotion_word = match.group(1)
            if emotion_word in EMOTION_AROUSAL_SCORES:
                matched_scores.append(min(1.0, EMOTION_AROUSAL_SCORES[emotion_word] + 0.1))

        if "i can't believe" in lowered_text:
            matched_scores.append(0.5)
        if any(phrase in lowered_text for phrase in ("i need to", "i have to", "i must")):
            matched_scores.append(0.3)
        if "finally" in lowered_text:
            matched_scores.append(0.3)
        if _LOVE_HATE_PATTERN.search(lowered_text):
            matched_scores.append(0.5)

        exclamation_bonus = min(0.15, text.count("!") * 0.05)
        caps_bonus = min(0.15, len(_ALL_CAPS_PATTERN.findall(text)) * 0.05)
        punctuation_bonus = exclamation_bonus + caps_bonus

        if matched_scores:
            arousal = float(np.mean(matched_scores)) + punctuation_bonus
        elif len(words) >= 5:
            arousal = 0.05 + punctuation_bonus
        else:
            arousal = punctuation_bonus
        return _clamp(arousal, 0.0, 1.0)


class AgencyExtractor:
    def __init__(self, nlp: Any | None = None, model_name: str = "en_core_web_sm"):
        self.nlp = nlp if nlp is not None else _load_spacy_model(model_name)

    def extract(self, text: str) -> float:
        doc = self.nlp(text)
        active = 0
        passive = 0

        for token in doc:
            dep = getattr(token, "dep_", "")
            head_pos = getattr(getattr(token, "head", None), "pos_", "")
            if dep in {"nsubj", "csubj"} and head_pos in {"VERB", "AUX"}:
                active += 1
            if dep in {"nsubjpass", "csubjpass", "auxpass"}:
                passive += 1

        total = active + passive
        if total == 0:
            return 0.0
        return _clamp((active - passive) / total, -1.0, 1.0)


class SelfFocusExtractor:
    def extract(self, text: str) -> float:
        words = [word.lower() for word in tokenize_words(text)]
        pronouns = [word for word in words if word in ALL_PRONOUNS]
        if not pronouns:
            return 0.5
        first_person = sum(word in FIRST_PERSON_PRONOUNS for word in pronouns)
        return _clamp(first_person / len(pronouns), 0.0, 1.0)


class SocialOrientationExtractor:
    def __init__(self, nlp: Any | None = None, model_name: str = "en_core_web_sm"):
        self.nlp = nlp if nlp is not None else _load_spacy_model(model_name)

    def extract(self, text: str) -> float:
        doc = self.nlp(text)
        tokens = [token for token in doc if not getattr(token, "is_space", False)]
        token_count = max(len(tokens), 1)
        person_entities = sum(
            1 for ent in getattr(doc, "ents", ()) if getattr(ent, "label_", "") == "PERSON"
        )
        third_person = sum(
            getattr(token, "text", "").lower() in THIRD_PERSON_PRONOUNS for token in tokens
        )
        return _clamp((person_entities + third_person) / token_count, 0.0, 1.0)


class ActionRatioExtractor:
    def __init__(self, nlp: Any | None = None, model_name: str = "en_core_web_sm"):
        self.nlp = nlp if nlp is not None else _load_spacy_model(model_name)

    def extract(self, text: str) -> float:
        doc = self.nlp(text)
        action_count = 0
        cognitive_count = 0

        for token in doc:
            if getattr(token, "pos_", "") not in {"VERB", "AUX"}:
                continue
            lemma = getattr(token, "lemma_", getattr(token, "text", "")).lower()
            if lemma in ACTION_VERBS:
                action_count += 1
            elif lemma in COGNITIVE_STATIVE_VERBS:
                cognitive_count += 1

        total = action_count + cognitive_count
        if total == 0:
            return 0.5
        return _clamp(action_count / total, 0.0, 1.0)


class SignalExtractor:
    def __init__(
        self,
        semantic_embedder: SemanticEmbedder | None = None,
        valence_extractor: ValenceExtractor | None = None,
        arousal_extractor: ArousalExtractor | None = None,
        agency_extractor: AgencyExtractor | None = None,
        self_focus_extractor: SelfFocusExtractor | None = None,
        social_orientation_extractor: SocialOrientationExtractor | None = None,
        action_ratio_extractor: ActionRatioExtractor | None = None,
        nlp: Any | None = None,
        spacy_model_name: str = "en_core_web_sm",
    ):
        self.semantic_embedder = semantic_embedder or SemanticEmbedder()
        self.valence_extractor = valence_extractor or ValenceExtractor()
        self.arousal_extractor = arousal_extractor or ArousalExtractor()
        self.self_focus_extractor = self_focus_extractor or SelfFocusExtractor()

        needs_shared_nlp = (
            agency_extractor is None
            or social_orientation_extractor is None
            or action_ratio_extractor is None
        )
        shared_nlp = nlp
        if needs_shared_nlp and shared_nlp is None:
            shared_nlp = _load_spacy_model(spacy_model_name)

        self.agency_extractor = agency_extractor or AgencyExtractor(nlp=shared_nlp)
        self.social_orientation_extractor = social_orientation_extractor or SocialOrientationExtractor(
            nlp=shared_nlp
        )
        self.action_ratio_extractor = action_ratio_extractor or ActionRatioExtractor(
            nlp=shared_nlp
        )

    def extract(self, entry: JournalEntry) -> SignalVector:
        text = entry.text
        return SignalVector(
            embedding=self.semantic_embedder.embed(text),
            valence=self.valence_extractor.extract(text),
            arousal=self.arousal_extractor.extract(text),
            agency=self.agency_extractor.extract(text),
            self_focus=self.self_focus_extractor.extract(text),
            social_orientation=self.social_orientation_extractor.extract(text),
            action_ratio=self.action_ratio_extractor.extract(text),
        )


__all__ = [
    "SemanticEmbedder",
    "ValenceExtractor",
    "ArousalExtractor",
    "AgencyExtractor",
    "SelfFocusExtractor",
    "SocialOrientationExtractor",
    "ActionRatioExtractor",
    "SignalExtractor",
    "tokenize_words",
]
