import json
import logging
from typing import Any, Dict, Optional

from GramAddict.core.perception.spatial_parser import SpatialNode

logger = logging.getLogger(__name__)


def _parse_yes_no(response: str) -> Optional[bool]:
    """Parses a VLM response to find a definitive YES or NO without substring-matching 'not' or 'now'."""
    text = response.strip()

    # Try parsing as JSON first
    if text.startswith("{"):
        try:
            data = json.loads(text)
            for k, v in data.items():
                if str(k).strip().upper() == "YES" or str(v).strip().upper() == "YES":
                    return True
                if str(k).strip().upper() == "NO" or str(v).strip().upper() == "NO":
                    return False
                if str(k).strip().lower() == "success" and isinstance(v, bool):
                    return v

            # If it is valid JSON but we couldn't definitively find YES/NO,
            # do NOT fall through to text matching
            return None
        except Exception:
            # Prevent JSON parsing fall-throughs
            return None

    text_lower = text.lower()
    if text_lower.startswith("yes"):
        return True
    if text_lower.startswith("no") and not text_lower.startswith("now") and not text_lower.startswith("not"):
        return False

    return None


# FSD Architecture: No static string dictionaries.
# The bot relies 100% on learned confidence and VLM/Delta verification.


class ActionMemory:
    """
    Handles the caching, tracking, and negative reinforcement (unlearning) of UI interactions.
    Decouples the memory layer from the core parsing engine.
    """

    def __init__(self, ui_memory=None, context_memory=None):
        # We optionally inject UIMemoryDB and ContextMemoryDB to decouple tests
        if ui_memory is None:
            from GramAddict.core.qdrant_memory import UIMemoryDB

            self.ui_memory = UIMemoryDB()
        else:
            self.ui_memory = ui_memory

        if context_memory is None:
            from GramAddict.core.qdrant_memory import ContextMemoryDB

            self.context_memory = ContextMemoryDB()
        else:
            self.context_memory = context_memory

        self._last_click_context: Optional[Dict[str, Any]] = None

    def track_click(self, intent: str, node: SpatialNode, xml_context: str = "", screen_type: str = "UNKNOWN"):
        """Stores the context of a click before it's actually performed."""
        semantic_string = f"text: '{node.text}', desc: '{node.content_desc}', id: '{node.resource_id}'"

        self._last_click_context = {
            "intent": intent,
            "node_dict": node.to_dict(),
            "semantic_string": semantic_string,
            "xml_context": xml_context,
            "screen_type": screen_type,
        }
        logger.debug(f"🧠 [ActionMemory] Tracking tentative click for intent: '{intent}' -> {semantic_string}")

    def confirm_click(self, intent: str = None):
        """Positive Reinforcement: Confirms the last click was successful.

        Guard: Refuses to store in Qdrant if the clicked element does not
        semantically match the intent. Prevents memory poisoning.
        """
        ctx = self._last_click_context
        if not ctx:
            return

        if intent and ctx["intent"] != intent:
            return

        # Zero-Trust FSD: No semantic string mismatch guards here.
        # If the VLM/Delta verification passed, we trust it and learn.

        logger.info(
            f"✅ [ActionMemory] Confirming success for '{ctx['intent']}'. Boosting confidence.",
            extra={"color": "\x1b[32m"},
        )

        # Store or boost in Qdrant
        try:
            # Check if it exists first
            existing = self.ui_memory.retrieve_memory(ctx["intent"], ctx["xml_context"])
            if existing:
                self.ui_memory.boost_confidence(ctx["intent"], ctx["xml_context"])
            else:
                self.ui_memory.store_memory(ctx["intent"], ctx["xml_context"], ctx["node_dict"])
            # Boost context confidence
            screen_type = ctx.get("screen_type", "UNKNOWN")
            self.context_memory.update_confidence(ctx["intent"], screen_type, delta=0.2)

        except Exception as e:
            logger.warning(f"Failed to confirm click in Qdrant: {e}")

        self._last_click_context = None

    def reject_click(self, intent: str = None):
        """Negative Reinforcement: Penalizes a failed click (Unlearning)."""
        ctx = self._last_click_context
        if not ctx:
            return

        if intent and ctx["intent"] != intent:
            return

        logger.warning(
            f"❌ [ActionMemory] Click failed for '{ctx['intent']}'. Applying penalty.", extra={"color": "\x1b[31m"}
        )

        try:
            self.ui_memory.decay_confidence(ctx["intent"], ctx["xml_context"])

            # Decay context confidence
            screen_type = ctx.get("screen_type", "UNKNOWN")
            self.context_memory.update_confidence(ctx["intent"], screen_type, delta=-0.2)

        except Exception as e:
            logger.warning(f"Failed to decay confidence in Qdrant: {e}")

        self._last_click_context = None

    def _compute_structural_delta(self, pre_xml: str, post_xml: str) -> dict:
        """Computes a semantic diff between two XML states."""
        import re

        pre_ids = set(re.findall(r'resource-id="([^"]+)"', pre_xml))
        post_ids = set(re.findall(r'resource-id="([^"]+)"', post_xml))

        pre_selected = set(re.findall(r'selected="true"[^>]*resource-id="([^"]+)"', pre_xml))
        post_selected = set(re.findall(r'selected="true"[^>]*resource-id="([^"]+)"', post_xml))

        return {
            "new_ids": post_ids - pre_ids,
            "removed_ids": pre_ids - post_ids,
            "selection_changed": pre_selected != post_selected,
            "id_delta_count": len(post_ids.symmetric_difference(pre_ids)),
        }

    def verify_success(
        self, intent: str, pre_click_xml: str, post_click_xml: str, device=None, confidence: float = 0.0
    ) -> Optional[bool]:
        """
        Structural and Visual verification: Did the UI actually change after the click?
        """
        intent_lower = intent.lower()

        # ALL HARDCODED UI STRUCTURAL VERIFICATION GUARDS HAVE BEEN PURGED!
        # Rule: ZERO MAINTENANCE. We do not hardcode Resource IDs to verify if a navigation
        # was successful (e.g., checking for 'profile_header_container' or 'main_feed_action_bar').
        # Success verification MUST rely entirely on the VLM visual feedback and the Structural Delta diff.

        state_toggles = ["like", "save", "follow", "heart"]
        is_toggle = any(t in intent_lower for t in state_toggles)

        # P0-1 Bypass Gate removed in FSD architecture.
        # We NO LONGER bypass VLM verification via string matching.
        # If confidence is < 0.95, we always do VLM or Delta verification.

        # ── VLM Verification (soft signal, NOT sole authority) ──

        # If we are highly confident (e.g. pulled from Qdrant memory), bypass heavy VLM
        vlm_verdict = None
        if device and confidence < 0.95:
            logger.info(
                f"👁️ [ActionMemory] Confidence ({confidence:.2f}) < 0.95. Handing over verification for '{intent}' to VLM visual analysis..."
            )
            from GramAddict.core.perception.semantic_evaluator import SemanticEvaluator

            evaluator = SemanticEvaluator()

            # Build context of what was actually clicked
            clicked_context = ""
            if self._last_click_context:
                clicked_context = f"The element that was tapped: {self._last_click_context['semantic_string']}. "

            prompt = (
                f"The user just attempted to perform the action: '{intent}'. "
                f"{clicked_context}"
                f"Look at the current screen carefully. Was the action successful? "
            )
            if is_toggle:
                prompt += (
                    "If the intent was 'follow', does the button now indicate 'Following' or 'Requested'? "
                    "If it was 'like', is the heart icon clearly active/red? "
                    "If the screen shifted completely to a profile when you just wanted to like/follow from a feed, it FAILED. "
                    "If the tapped element does NOT sound like a like/follow button (e.g. it's a caption, comment field, or post content), it FAILED. "
                )
            else:
                prompt += (
                    f"Does the current screen match the expected outcome of '{intent}'? "
                    f"For example, if the intent was to open a post/photo, are you looking at a post view (not a user profile or story)? "
                    f"If the intent was to open a profile, are you on a profile page? "
                    f"If the intent was to go back, are you on the previous screen? "
                )
            prompt += 'Answer ONLY with a valid JSON object exactly matching this schema: {"success": true} or {"success": false}. DO NOT add any other keys.'

            try:
                screenshot = device.get_screenshot_b64()
                if not screenshot:
                    raise ValueError("No screenshot available from device")
                response = evaluator._query_vlm(prompt, screenshot)

                vlm_verdict = _parse_yes_no(response) if response else None

                if vlm_verdict is True:
                    logger.debug(f"🧠 [ActionMemory] VLM visually confirmed success for '{intent}'.")
                    return True
                elif vlm_verdict is False:
                    # VLM says false — but small local VLMs (7B) are unreliable.
                    # Do NOT trust this blindly. Fallthrough to structural delta verification
                    # which is the ground-truth tiebreaker.
                    logger.info(
                        f"🧠 [ActionMemory] VLM says '{intent}' failed — but VLM is unreliable. "
                        "Falling through to structural delta for ground-truth verification."
                    )
                    # DO NOT return False here — let structural delta decide
                else:
                    logger.debug(
                        f"🧠 [ActionMemory] VLM response for '{intent}' was not YES/NO "
                        f"(got: '{response[:80]}...'). Falling through to structural verification."
                    )
            except Exception as e:
                logger.error(f"Failed to query VLM for visual verification: {e}")
                # Fallthrough to structural delta if VLM crashes

        # Pre-Structural Semantic Gate removed in FSD architecture.
        # If the delta matches, we trust it. No more static string restrictions.

        # ── Structural Delta Verification ──
        diff = self._compute_structural_delta(pre_click_xml, post_click_xml)

        if is_toggle:
            if diff["id_delta_count"] > 10:
                logger.warning(
                    f"⚠️ [ActionMemory] Massive structural shift ({diff['id_delta_count']} nodes) for state-toggle '{intent}'. Navigated away by mistake? Verification FAIL."
                )
                return False
            if diff["id_delta_count"] > 0 or diff["selection_changed"]:
                logger.debug(f"🧠 [ActionMemory] Structural delta detected for toggle '{intent}'. Verification PASS.")
                return True
            logger.warning(f"⚠️ [ActionMemory] Zero structural shift for state-toggle '{intent}'. Verification FAIL.")
            return False

        # ── Non-Toggle Structural Delta ──
        # A click (non-toggle) should change something on screen (e.g., popup, screen transition)
        # Even scrolling will load new items (so new IDs will be present).
        # We look for at least a few ID changes. Let's say >= 2 to be safe against random background updates,
        # or if the selection changed.
        if diff["id_delta_count"] >= 1 or diff["selection_changed"]:
            logger.debug(
                f"🧠 [ActionMemory] Structural change detected ({diff['id_delta_count']} nodes) for '{intent}'. Verification PASS."
            )
            return True

        logger.warning(
            f"⚠️ [ActionMemory] Insufficient structural change (delta=0) for non-toggle '{intent}'. Verification FAIL."
        )
        return False
