import logging
import math
import random
from typing import Optional

from colorama import Fore

from GramAddict.core.llm_provider import query_llm
from GramAddict.core.qdrant_memory import CommentMemoryDB, ContentMemoryDB, ParasocialCRMDB, PersonaMemoryDB

logger = logging.getLogger(__name__)


class ResonanceEngine:
    """
    The Aesthetic Oracle — Real AI Content Evaluation.

    Calculates semantic alignment (Resonance Score) between the bot's
    configured persona interests and target content using vector embeddings.

    This drives ALL downstream decisions:
    - Like probability (score >= 0.35)
    - Comment probability (score >= 0.8)
    - Rabbit Hole / profile visit (score >= 0.9)
    - Dopamine spike intensity
    - Darwin dwell time modulation
    """

    def __init__(self, my_username: str, persona_interests: list[str] = None, crm: ParasocialCRMDB = None):
        self.my_username = my_username
        self.content_memory = ContentMemoryDB()
        self.persona_memory = PersonaMemoryDB()
        self.crm = crm
        self.threshold = 0.5

        # The persona vector is the mathematical identity of what content we care about.
        # It's generated from config's persona_interests and cached for the entire session.
        self._persona_vector: Optional[list] = None
        self._persona_interests = persona_interests or []

        # Bootstrap persona on init
        if self._persona_interests:
            self._bootstrap_persona()

    def _bootstrap_persona(self):
        """
        Generates and caches the persona embedding from configured interests.
        Called once on init. The persona vector is what every post is compared against.
        """
        persona_text = f"Content about: {', '.join(self._persona_interests)}"
        self._persona_vector = self.content_memory._get_embedding(persona_text)

        if self._persona_vector:
            # Store in PersonaMemoryDB for persistence across sessions
            self.persona_memory.store_persona_insight(
                "interests", f"Core niche interests: {', '.join(self._persona_interests)}"
            )
            logger.info(
                f"✨ [Resonance Oracle] Persona vector initialized from config: {self._persona_interests}",
                extra={"color": f"{Fore.MAGENTA}"},
            )
        else:
            logger.warning(
                "✨ [Resonance Oracle] Could not generate persona embedding. Falling back to neutral scoring."
            )

    def update_identity(self, persona: list, vibe: str):
        """Dynamically update the core agent identity and embeddings during a session"""
        self._persona_interests = persona

        # Build embedding for updated persona
        combined_text = " ".join(self._persona_interests)
        new_vector = self.content_memory._get_embedding(combined_text)

        if new_vector:
            self._persona_vector = new_vector
            self.persona_memory.store_persona_insight(
                "interests", f"Dynamically updated interests: {', '.join(self._persona_interests)}"
            )
            logger.info(
                f"✨ [Resonance Oracle] Identity dynamically updated! New Persona: {self._persona_interests} | Vibe: {vibe}",
                extra={"color": f"{Fore.MAGENTA}"},
            )
        else:
            logger.warning(
                "✨ [Resonance Oracle] Failed to build embedding for new identity. Retaining previous state."
            )

    def _classification_to_score(self, classification: str) -> float:
        """Maps semantic classification labels to numerical scores."""
        mapping = {"high": 0.85, "medium": 0.5, "low": 0.2}
        return mapping.get(classification.lower(), 0.5)

    def _cosine_similarity(self, v1: list, v2: list) -> float:
        """Pure python cosine similarity — no numpy dependency."""
        if not v1 or not v2 or len(v1) != len(v2):
            return 0.0
        dot = sum(a * b for a, b in zip(v1, v2))
        mag1 = math.sqrt(sum(a * a for a in v1))
        mag2 = math.sqrt(sum(b * b for b in v2))
        if mag1 == 0 or mag2 == 0:
            return 0.0
        return dot / (mag1 * mag2)

    def calculate_resonance(self, post_content: dict) -> float:
        """
        Real AI resonance score based on embedding cosine similarity.
        """
        username = post_content.get("username", "Unknown")
        description = post_content.get("description", "")

        logger.info(f"✨ [Resonance Oracle] Evaluating content from @{username}...", extra={"color": f"{Fore.MAGENTA}"})

        # Build a rich text representation of the post

        description = post_content.get("description", "")
        caption = post_content.get("caption", "")
        username = post_content.get("username", "")

        content_text = " ".join(filter(None, [description, caption])).strip()

        if not content_text or len(content_text) < 5:
            logger.debug("✨ [Resonance] Post has no extractable content. Neutral score.")
            return 0.5  # Neutral — can't evaluate what we can't see

        # 0. Ads are now checked upstream structurally via `is_ad(xml)` in bot_flow.
        #    This prevents false positives from users writing 'Werbung' in non-ad contexts.

        # 1. Check ContentMemoryDB cache — have we seen nearly identical content?
        cached = self.content_memory.get_cached_evaluation(content_text)
        if cached:
            score = self._classification_to_score(cached.get("classification", "medium"))
            logger.info(
                f"✨ [Resonance Cache Hit] '{content_text[:40]}...' → {score*100:.1f}%",
                extra={"color": f"{Fore.MAGENTA}"},
            )
            return score

        # 2. No persona vector? Can't do real evaluation.
        if not self._persona_vector:
            logger.debug("✨ [Resonance] No persona vector. Configure persona_interests in config.yml.")
            return 0.5

        # 3. Generate embedding of the post content
        post_vector = self.content_memory._get_embedding(content_text)
        if not post_vector:
            return 0.5

        # 4. Cosine similarity against persona = resonance score
        raw_score = self._cosine_similarity(post_vector, self._persona_vector)

        # Normalize: text-embedding-3-small cosine similarity for text embeddings typically ranges 0.15 (completely distinct) to 0.55 (very matched, but not literal identical copies)
        # Map this to a more useful 0.0-1.0 range
        score = max(0.0, min(1.0, (raw_score - 0.15) / 0.30))

        # ── Contextual Empathy Filter ──
        # If the content is tragic or highly controversial, we must NOT like it, regardless of interest alignment.
        score = self._apply_empathy_filter(content_text, score)

        # 5. Store evaluation in ContentMemoryDB for future cache hits
        classification = "high" if score > 0.7 else "medium" if score > 0.4 else "low"
        self.content_memory.store_evaluation(
            content_text[:500],  # Cap length for storage
            classification,
            f"Resonance: {score:.3f} (raw cosine: {raw_score:.3f})",
        )

        # 6. Feed the Parasocial CRM
        if self.crm and username:
            intent = f"aesthetic_evaluation_{classification}"
            # Stage mapping: high resonance -> stage 1 (Curiosity)
            new_stage = 1 if classification == "high" else None
            self.crm.log_interaction(username, intent, new_stage=new_stage)

        logger.info(
            f"✨ [Resonance Oracle] '{content_text[:50]}...' → {score*100:.1f}% ({classification})",
            extra={"color": f"{Fore.MAGENTA}"},
        )
        return score

    def _apply_empathy_filter(self, text: str, current_score: float) -> float:
        """
        Scans for tragic or sensitive keywords. If found, suppresses resonance
        to prevent bot-like behavior of liking inappropriate content.
        """
        tragic_keywords = [
            # English
            "rip",
            "rest in peace",
            "tragedy",
            "died",
            "killed",
            "accident",
            "shooting",
            "funeral",
            "sad news",
            "memorial",
            "cancer",
            "disease",
            "breaking news",
            # German
            "ruhe in frieden",
            "verstorben",
            "tragödie",
            "unfall",
            "tot",
            "beerdigung",
            "trauer",
            "krebs",
            "krankheit",
        ]

        text_lower = text.lower()
        if any(f" {word} " in f" {text_lower} " for word in tragic_keywords):
            logger.warning(
                "🛡️ [Empathy Filter] Tragic/Sensitive content detected. Suppressing resonance to prevent blind liking."
            )
            # Drastically reduce score to "low resonance" zone (avoid liking)
            return min(current_score, 0.2)

        return current_score

    def judge_interaction(self, score: float) -> bool:
        """
        Binary engagement gate.

        Returns True if the resonance score is high enough to warrant any
        interaction (like, comment, profile visit).  The threshold mirrors
        the like-gate used in the feed loop (bot_flow.py, res_score >= 0.35).

        Args:
            score: Resonance score in [0.0, 1.0] as returned by calculate_resonance().

        Returns:
            True  → score qualifies for engagement.
            False → score is too low; skip this post.
        """
        return score >= 0.35

    def get_suggested_action(self, username: str, base_resonance: float) -> str:
        """
        [Phase 2] High-fidelity relationship escalation.
        Determines the 'best' interaction based on content resonance AND
        past engagement history (CRM).
        """
        if not self.crm or not username:
            # Default logic: Like if resonance is good enough
            if base_resonance >= 0.7:
                return "LIKE"
            return "SKIP"

        relationship = self.crm.get_relationship_stage(username)
        stage = relationship.get("stage", 0)

        # ── Escalation Logic ──
        # Stage 0: Awareness (Seen/Cold) -> Only Like
        # Stage 1: Curiosity (Interacted once) -> Like + Comment
        # Stage 2: Rapport (Multiple interactions) -> Like + Comment + Follow
        # Stage 3: Conversion (Max relationship) -> High-frequency engagement

        if stage == 0:
            if base_resonance >= 0.85:
                return "COMMENT"  # Instant hook if amazing
            if base_resonance >= 0.60:
                return "LIKE"
        elif stage == 1:
            if base_resonance >= 0.70:
                return "COMMENT"
            if base_resonance >= 0.40:
                return "LIKE"
        elif stage >= 2:
            if base_resonance >= 0.60:
                return "COMMENT"
            if base_resonance >= 0.30:
                return "LIKE"

        return "SKIP"

    # ── [Phase 3] Engagement Decision Logic ──

    def wants_to_reply(self, base_resonance: float) -> bool:
        """Decides if the bot should reply to a comment."""
        if base_resonance < 0.75:
            return False
        # CRM stage 1+ increases reply chance
        return random.random() < 0.35

    def wants_to_deep_engage(self, base_resonance: float) -> bool:
        """Decides if the bot should click through to a commenter profile."""
        if base_resonance < 0.8:
            return False
        return random.random() < 0.25

    def extract_and_learn_comments(self, xml_hierarchy: str, configs, author: str = "unknown", images_b64: list = None):
        """
        Phase 10: RAG Comment Learning Implementation
        Extracts comments from the UI hierarchy, filters them using the assigned VLM against
        configured blacklists and vibes, and stores them in Qdrant CommentMemoryDB.
        Supports True Vision Context (images_b64) to mathematically align comments to visual aesthetics.
        """
        if not configs or not getattr(configs.args, "ai_learn_comments", False):
            return

        vibe = getattr(configs.args, "ai_vibe", "")
        blacklist = getattr(configs.args, "ai_blacklist_topics", "")
        if not vibe:
            return  # No vibe to learn

        logger.info(
            f"🧠 [Comment Learning] Extracting comments matching vibe: '{vibe}'...", extra={"color": f"{Fore.CYAN}"}
        )

        # 1. Very basic semantic extraction (grab text nodes that look like comments)
        raw_comments = []

        try:
            import xml.etree.ElementTree as ET

            root = ET.fromstring(xml_hierarchy)
            for node in root.iter("node"):
                # 1. Block System UI (Notifications, WiFi, etc)
                pkg = node.get("package", "").lower()
                if pkg != "com.instagram.android":
                    continue

                text = node.get("text", "")
                content_desc = node.get("content-desc", "")
                val = (text if text else content_desc).strip()
                res_id = node.get("resource-id", "").lower()

                # 2. Heuristics: Only target comment text views
                is_comment_node = "comment" in res_id or "textview" in res_id

                # 3. Block accessibility garbage & UI labels
                # Zero-Maintenance: Only structural patterns. Short strings
                # (< 5 chars) from UI buttons are blocked by length, not by
                # translating every possible language.
                is_ui_junk = (
                    val.lower().startswith("go to")
                    or val.lower().startswith("tap to")
                    or "actions for this post" in val.lower()
                    or len(val.strip()) < 3
                )

                # Block known English UI action labels.
                # We intentionally do NOT add German/Spanish/etc translations.
                # Instead, we rely on the structural `is_comment_node` filter
                # above + length heuristic to catch non-comment UI elements.
                blocked_exact = [
                    "reply",
                    "like",
                    "view replies",
                    "see translation",
                    "hide replies",
                    "view all comments",
                    "send",
                ]

                if val and len(val) > 2 and is_comment_node and not is_ui_junk:
                    if val.lower() not in blocked_exact:
                        raw_comments.append(val)
        except Exception as e:
            logger.error(f"🧠 [Comment Learning] Failed to parse XML: {e}")
            return

        if not raw_comments:
            logger.debug("🧠 [Comment Learning] No legible comments found in UI.")
            return

        # Deduplicate and limit
        raw_comments = list(set(raw_comments))[:10]
        logger.debug(
            f"🧠 [Comment Learning] Scraped {len(raw_comments)} potential comment nodes. Passing to Condenser..."
        )

        logger.debug(f"🧠 [Comment Learning] Raw texts passed to Condenser:\n{chr(10).join(raw_comments)}")

        # 2. Filter via VLM Condenser
        prompt = (
            f"Evaluate these Instagram comments. Your goal is to identify comments that generally match this vibe while blocking SPAM, UI junk, and harmful topics.\n"
            f"VIBE = '{vibe}'\n"
            f"BLACKLIST = {blacklist}\n\n"
            f"Comments to evaluate:\n{chr(10).join(['- ' + c for c in raw_comments])}\n\n"
            "Return a JSON object with 'evaluations' array. Each item must have 'text', 'has_blacklist_words' (bool), and 'keep' (bool).\n"
            "Set 'keep' to true if the comment feels authentic and matches the vibe.\n"
            "Set 'keep' to false only for clear spam, bots, UI buttons, or blacklist violations.\n"
        )

        model = getattr(configs.args, "ai_condenser_model")
        url = getattr(configs.args, "ai_condenser_url")

        try:
            import json

            system = "You are a precise JSON filtering agent."
            # Fix: kwargs match query_llm signature EXACTLY to evade TypeError
            response_dict = query_llm(
                url=url,
                model=model,
                prompt=prompt,
                system=system,
                format_json=True,
                images_b64=images_b64,
                max_tokens=600,
                temperature=0.1,
            )
            if not response_dict or "response" not in response_dict:
                return

            response_text = response_dict["response"]
            # DEBUG
            logger.debug(f"DEBUG CONDENSER RAW: {response_text}")

            # Parse json gracefully
            if isinstance(response_text, str):
                clean_json = response_text.strip()
                if clean_json.startswith("```json"):
                    clean_json = clean_json[7:]
                if clean_json.endswith("```"):
                    clean_json = clean_json[:-3]
                try:
                    learned_comments = json.loads(clean_json.strip())
                except json.JSONDecodeError:
                    logger.error("🧠 [Comment Learning] LLM returned invalid JSON.")
                    return
            else:
                # In case expect_json already returned a parsed list somehow, though extract_json returns str
                learned_comments = response_text

            # Filter the dict based on evaluations array
            if isinstance(learned_comments, dict):
                valid_list = []
                evals = learned_comments.get("evaluations", [])
                for ev in evals:
                    # Qwen 3.5 correctly identifies 'has_blacklist_words' but hallucinates 'keep': true
                    has_spam = ev.get("has_blacklist_words", False)
                    keep = ev.get("keep", True)
                    if not has_spam and keep:
                        valid_list.append(ev.get("text"))
                learned_comments = valid_list

            if not isinstance(learned_comments, list):
                logger.error(
                    f"🧠 [Comment Learning] Condenser failed to return a valid JSON structure: {learned_comments}"
                )
                return

            if not learned_comments:
                logger.info(
                    "🧠 [Comment Learning] Condenser rejected all scraped comments (did not align with vibe or hit blacklist).",
                    extra={"color": f"{Fore.YELLOW}"},
                )
                return

            logger.info(
                f"🧠 [Comment Learning] Condenser approved {len(learned_comments)} comments. Persisting to Qdrant...",
                extra={"color": f"{Fore.GREEN}"},
            )

            # 3. Store the passing comments into Qdrant
            comment_db = CommentMemoryDB()
            stored = 0
            for c in learned_comments:
                if isinstance(c, str) and len(c) > 5:
                    logger.debug(f"   👉 Storing: '{c}'")
                    comment_db.store_comment(text=c, vibe=vibe, author=author)
                    stored += 1

            if stored > 0:
                logger.info(
                    f"✅ [Comment Vector Sync] Successfully embedded {stored} high-vibe comments into memory.",
                    extra={"color": f"{Fore.GREEN}"},
                )

        except Exception as e:
            logger.error(f"🧠 [Comment Learning] Condenser failed: {e}")
