"""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")

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",
}

INTENSE_WORDS = {
    "amazing",
    "wonderful",
    "incredible",
    "terrible",
    "awful",
    "horrible",
    "fantastic",
    "furious",
    "ecstatic",
    "devastated",
    "stunning",
    "wild",
    "deeply",
    "extremely",
    "totally",
    "absolutely",
    "intensely",
    "super",
}

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 = tokenize_words(text)
        if not words:
            return 0.0

        lowered = [word.lower() for word in words]
        intense_word_hits = sum(word in INTENSE_WORDS for word in lowered)
        caps_hits = len(_ALL_CAPS_PATTERN.findall(text))
        exclamation_hits = text.count("!")

        raw = (
            intense_word_hits + caps_hits + (2.0 * exclamation_hits)
        ) / max(len(words), 1)
        normalized = raw * 1.2
        return _clamp(normalized, 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",
]
