import logging
from typing import Optional

from GramAddict.core.perception.action_memory import ActionMemory
from GramAddict.core.perception.intent_resolver import IntentResolver
from GramAddict.core.perception.semantic_evaluator import SemanticEvaluator
from GramAddict.core.perception.spatial_parser import SpatialNode, SpatialParser

logger = logging.getLogger(__name__)


class TelepathicEngine:
    """
    The Spatial Perception Engine (Facade)

    Replaces the legacy 2100+ LoC Regex/XML engine with a structural Topo-Graph approach.
    Delegates all responsibilities to SOLID, testable sub-modules.
    """

    _instance = None

    # Delegate Modules
    _parser: SpatialParser
    _resolver: IntentResolver
    _memory: ActionMemory
    _evaluator: SemanticEvaluator

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    @classmethod
    def reset(cls):
        cls._instance = None

    def __init__(self):
        self._parser = SpatialParser()
        self._resolver = IntentResolver()
        self._memory = ActionMemory()
        self._evaluator = SemanticEvaluator()

    def wipe(self):
        """Wipe all persistent state for Blank Start."""
        logger.warning("🔥 [TelepathicEngine] Wiping state (delegating to ActionMemory layer).")
        try:
            self._memory.ui_memory.wipe_collection()
        except Exception as e:
            logger.warning(f"Could not wipe Qdrant collection: {e}")

    # ──────────────────────────────────────────────
    # Core Resolution Engine
    # ──────────────────────────────────────────────

    def find_best_node(
        self,
        xml_string: str,
        intent_description: str,
        device=None,
        track: bool = True,
        exclude_bounds: list[str] = None,
        **kwargs,
    ) -> Optional[dict]:
        """
        Public facade for resolving a node.
        Translates Android UI bounds into standard GramAddict node dicts.
        """
        logger.debug(f"🧠 [SpatialEngine] Resolving intent: '{intent_description}'")

        # 1. Parse into Spatial Topology
        root = self._parser.parse(xml_string)
        if not root:
            logger.warning("Failed to parse Spatial Topology.")
            return None

        # 2. Extract interactable candidates
        candidates = self._parser.get_clickable_nodes(root)

        if exclude_bounds:
            filtered_candidates = []
            for c in candidates:
                bounds_str = f"[{c.x1},{c.y1}][{c.x2},{c.y2}]"
                if bounds_str not in exclude_bounds:
                    filtered_candidates.append(c)
            candidates = filtered_candidates

        # --- Active Keyboard Dismiss Guard ---
        # If keyboard is open but intent is NOT typing related -> dismiss it!
        intent_lower = intent_description.lower()
        if device and self._resolver.has_keyboard_open(candidates):
            typing_keywords = ["type", "message", "comment", "search", "write"]
            if not any(k in intent_lower for k in typing_keywords):
                logger.warning("⌨️ [TelepathicEngine] Keyboard detected during non-typing intent! Auto-dismissing.")
                import time
                device.back()
                time.sleep(1.0)
                # Re-fetch UI state
                xml_string = device.dump_hierarchy()
                if xml_string:
                    root = self._parser.parse(xml_string)
                    if root:
                        candidates = self._parser.get_clickable_nodes(root)

        # 3. Resolve intent against candidates
        best_node = self._resolver.resolve(intent_description, candidates, device=device)

        if not best_node:
            logger.warning(f"No viable nodes found for intent: '{intent_description}'")
            return None

        # 3.1 BUG 7 Fix: Semantic Guard for 'post media content'
        intent_lower = intent_description.lower()
        semantic_str = (
            (best_node.text or "") + " " + (best_node.content_desc or "") + " " + (best_node.resource_id or "")
        ).lower()
        if "post media content" in intent_lower:
            if "follow" in semantic_str.replace("_", " "):
                logger.warning("🚫 [SpatialEngine] VLM selected a 'Follow' button for 'post media content'. Blocked.")
                return None

        # 3.5 Following Button Guard
        if (
            "follow" in intent_description.lower()
            and "unfollow" not in intent_description.lower()
            and "following" not in intent_description.lower()
        ):
            semantic = (
                (best_node.text or "") + " " + (best_node.content_desc or "") + " " + (best_node.resource_id or "")
            )
            semantic = semantic.lower()
            # Zero-Maintenance: Only English UI states. resource_id never changes with locale.
            if "following" in semantic or "requested" in semantic:
                return {"skip": True, "semantic": "already_followed"}

        # 4. Track action
        if track:
            self._memory.track_click(intent_description, best_node, xml_string)

        # Translate to old GramAddict dict format for backward compatibility
        return self._translate_node(best_node)

    def _translate_node(self, node: SpatialNode) -> dict:
        """Adapts the new SpatialNode to the legacy GramAddict dictionary format."""
        return {
            "x": node.center_x,
            "y": node.center_y,
            "bounds": f"[{node.x1},{node.y1}][{node.x2},{node.y2}]",
            "text": node.text,
            "description": node.content_desc,
            "id": node.resource_id,
            "class": node.class_name,
            "semantic": node.content_desc or node.text or node.resource_id,
            "original_attribs": node.to_dict(),
        }

    def _extract_semantic_nodes(
        self, xml_string: str, semantic_query: str = None, threshold: float = 0.7, **kwargs
    ) -> list:
        """
        Legacy proxy for extracting semantic nodes.
        If a query is provided, it tries to find the best node matching it.
        If no query is provided, it returns all clickable nodes to support zero-latency feed loops.
        """
        if semantic_query:
            best_node = self.find_best_node(xml_string, semantic_query, **kwargs)
            return [best_node] if best_node else []

        # If no query, return all interactable nodes as dicts (used by zero latency loops)
        root = self._parser.parse(xml_string)
        if not root:
            return []
        nodes = self._parser.get_clickable_nodes(root)
        return [self._translate_node(n) for n in nodes]

    # ──────────────────────────────────────────────
    # Action Memory Delegation
    # ──────────────────────────────────────────────

    def confirm_click(self, intent: str = None):
        self._memory.confirm_click(intent)

    def reject_click(self, intent: str = None):
        self._memory.reject_click(intent)

    def decay_click(self, intent: str = None):
        self._memory.reject_click(intent)  # Alias to reject

    def verify_success(self, intent: str, post_click_xml: str, device=None, confidence: float = 0.0) -> bool:
        pre_click_xml = ""
        if self._memory._last_click_context:
            pre_click_xml = self._memory._last_click_context.get("xml_context", "")
        return self._memory.verify_success(intent, pre_click_xml, post_click_xml, device=device, confidence=confidence)

    # ──────────────────────────────────────────────
    # Semantic Evaluator Delegation
    # ──────────────────────────────────────────────

    def evaluate_grid_visuals(self, device, persona_interests: list[str]) -> Optional[dict]:
        root = self._parser.parse(device.dump_hierarchy())
        if not root:
            return None

        # Find grid-like visual nodes using structural markers
        all_nodes = self._parser.get_all_nodes(root)
        grid_candidates = []
        for n in all_nodes:
            rid = (n.resource_id or "").lower()
            desc = (n.content_desc or "").lower()

            # Structural anchor for Instagram grid items
            if "grid_card_layout_container" in rid:
                grid_candidates.append(n)
            # Fallback for older versions or profile grids if resource-id is missing
            elif not n.text and ("photo" in desc or "video" in desc) and (50000 < n.area < 400000):
                grid_candidates.append(n)

        best = self._evaluator.evaluate_grid_visuals(device, persona_interests, grid_candidates)
        if best:
            return self._translate_node(best)
        return None

    def evaluate_post_vibe(self, device, persona_interests: list[str]) -> Optional[dict]:
        return self._evaluator.evaluate_post_vibe(device, persona_interests)

    def evaluate_profile_vibe(self, device, persona_interests: list[str]) -> Optional[dict]:
        return self._evaluator.evaluate_profile_vibe(device, persona_interests)

    def classify_screen_content(self, xml_hierarchy: str, target_class: str) -> Optional[str]:
        return self._evaluator.classify_screen_content(xml_hierarchy, target_class)

    def _is_forbidden_action(self, node: dict) -> bool:
        """
        Layer 5: Forbids clicks on specific dangerous elements like the quick_capture root container.
        """
        res_id = node.get("resource_id", "").lower()
        if "quick_capture" in res_id:
            return True
        return False

    def _get_current_username(self) -> str:
        return ""

    def _is_instagram_context(self, xml_string: str) -> bool:
        return "com.instagram.android" in xml_string

    def _structural_sanity_check(self, node: dict, intent_description: str, screen_height: int = 2400) -> bool:
        """
        Structural guard to ensure nodes are in valid locations for their intents.
        """
        intent = intent_description.lower()
        y = node.get("y", 0)
        semantic = (node.get("semantic_string", "") or "").lower()

        # 1. Post Username Guard
        if "post username" in intent or "author username" in intent:
            if "story" in semantic:
                # E.g. "Your Story" circle at the top
                return False
            # Prevent tapping a search list item when looking for a post username
            if "row search user container" in semantic.replace("_", " "):
                return False
            # Prevent tapping bottom tabs
            if "tab" in semantic and "exclude bottom tabs" in intent:
                return False
            return True

        # 3.5 Media Content Guard
        if "post media content" in intent:
            # Prevent tapping a search keyword instead of a media post
            if "row search keyword title" in semantic.replace("_", " "):
                return False
            # Prevent tapping bottom tabs
            if "tab" in semantic and "exclude bottom tabs" in intent:
                return False

        # 3.6 Post Author Username Header Guard
        if "post author username header" in intent:
            # Prevent tapping the follow button when looking for the username
            if "follow button" in semantic.replace("_", " "):
                return False

        # 4. Profile Picture/Story Ring Guard
        if "story ring" in intent or "avatar" in intent:
            current_user = self._get_current_username()
            if current_user and current_user in semantic:
                # Don't click our own story
                return False

        # 5. Grid Item Guard
        if "grid item" in intent:
            if y > screen_height * 0.8:  # Too low
                return False
            if "tab" in semantic and "grid" not in semantic:
                # Not a grid item
                return False

        # 6. Block massive layout containers UNLESS specifically looking for feed/post
        MAX_CONTAINER_AREA = 500000
        area = node.get("area", 0)

        is_feed_or_post = "feed" in intent or "post" in intent
        is_grid_item = "grid" in intent or "list" in intent

        if area > MAX_CONTAINER_AREA:
            # If specifically looking for a grid item, REJECT massive containers (e.g., RecyclerViews)
            if is_grid_item:
                return False
            # Otherwise, only allow if looking for feed/post
            if not is_feed_or_post:
                return False

        return True
